mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-08-31 18:46:08 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfb243159d | |||
| 106a61c943 | |||
| e5b656e372 | |||
| 297e797a32 | |||
| 48a95da874 | |||
| 2fd1dc0ff9 | |||
| 45b6ecff05 | |||
| 672bcbae01 |
Vendored
+2
@@ -509,6 +509,8 @@ QWidget#contentRichDialog QLabel#label_title_rich {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QWidget#contentDialog QLabel#label_dialog {
|
QWidget#contentDialog QLabel#label_dialog {
|
||||||
|
background: #2E2E2E;
|
||||||
|
|
||||||
padding: 20px 65px;
|
padding: 20px 65px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-8
@@ -43,6 +43,7 @@ This guide will walk you through adding a new boolean toggle setting to Eden's c
|
|||||||
Firstly add your desired toggle:
|
Firstly add your desired toggle:
|
||||||
|
|
||||||
Example: `src/common/setting.h`
|
Example: `src/common/setting.h`
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
SwitchableSetting<bool> your_setting_name{linkage, false, "your_setting_name", Category::RendererExtensions};
|
SwitchableSetting<bool> your_setting_name{linkage, false, "your_setting_name", Category::RendererExtensions};
|
||||||
```
|
```
|
||||||
@@ -67,6 +68,7 @@ Common Categories:
|
|||||||
Add the toggle to the Qt UI, where you wish for it to appear and place it there.
|
Add the toggle to the Qt UI, where you wish for it to appear and place it there.
|
||||||
|
|
||||||
Example: `src/qt_common/config/shared_translation.cpp`
|
Example: `src/qt_common/config/shared_translation.cpp`
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
INSERT(Settings,
|
INSERT(Settings,
|
||||||
your_setting_name,
|
your_setting_name,
|
||||||
@@ -91,6 +93,7 @@ INSERT(Settings,
|
|||||||
Add where it should be in the settings.
|
Add where it should be in the settings.
|
||||||
|
|
||||||
Example: `src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt`
|
Example: `src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt`
|
||||||
|
|
||||||
```kts
|
```kts
|
||||||
RENDERER_YOUR_SETTING_NAME("your_setting_name"),
|
RENDERER_YOUR_SETTING_NAME("your_setting_name"),
|
||||||
```
|
```
|
||||||
@@ -106,6 +109,7 @@ RENDERER_YOUR_SETTING_NAME("your_setting_name"),
|
|||||||
Add the toggle to the Kotlin (Android) UI
|
Add the toggle to the Kotlin (Android) UI
|
||||||
|
|
||||||
Example: `src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt`
|
Example: `src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt`
|
||||||
|
|
||||||
```kts
|
```kts
|
||||||
put(
|
put(
|
||||||
SwitchSetting(
|
SwitchSetting(
|
||||||
@@ -123,6 +127,7 @@ put(
|
|||||||
Add your setting within the right category.
|
Add your setting within the right category.
|
||||||
|
|
||||||
Example: `src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt`
|
Example: `src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt`
|
||||||
|
|
||||||
```kts
|
```kts
|
||||||
add(BooleanSetting.RENDERER_YOUR_SETTING_NAME.key)
|
add(BooleanSetting.RENDERER_YOUR_SETTING_NAME.key)
|
||||||
```
|
```
|
||||||
@@ -137,6 +142,7 @@ add(BooleanSetting.RENDERER_YOUR_SETTING_NAME.key)
|
|||||||
Add your setting and description in the appropriate place.
|
Add your setting and description in the appropriate place.
|
||||||
|
|
||||||
Example: `src/android/app/src/main/res/values/strings.xml`
|
Example: `src/android/app/src/main/res/values/strings.xml`
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<string name="your_setting_name">Your Setting Display Name</string>
|
<string name="your_setting_name">Your Setting Display Name</string>
|
||||||
<string name="your_setting_name_description">Detailed description of what this setting does. Explain any caveats, requirements, or warnings here.</string>
|
<string name="your_setting_name_description">Detailed description of what this setting does. Explain any caveats, requirements, or warnings here.</string>
|
||||||
@@ -150,6 +156,7 @@ Now the UI part is done find a place in the code for the toggle,
|
|||||||
And use it to your heart's desire!
|
And use it to your heart's desire!
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
const bool your_value = Settings::values.your_setting_name.GetValue();
|
const bool your_value = Settings::values.your_setting_name.GetValue();
|
||||||
|
|
||||||
@@ -196,25 +203,31 @@ Common advantages recap:
|
|||||||
|
|
||||||
#### Accessing Debug Knobs (dev side)
|
#### Accessing Debug Knobs (dev side)
|
||||||
|
|
||||||
Use the `Settings::getDebugKnobAt(u8 i)` function to check if a specific bit is set:
|
Use the `Settings::GetDebugKnobAt(u8 i)` function to check if a specific bit is set:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
//cpp side
|
//cpp side
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
|
|
||||||
|
//To use it as a general purpose uint var:
|
||||||
|
unsigned int debug_knobs = Settings::values.debug_knobs.GetValue();
|
||||||
|
|
||||||
// Check if bit 0 is set
|
// Check if bit 0 is set
|
||||||
bool feature_enabled = Settings::getDebugKnobAt(0);
|
bool feature_enabled = Settings::GetDebugKnobAt(0);
|
||||||
|
|
||||||
// Check if bit 15 is set
|
// Check if bit 15 is set
|
||||||
bool another_feature = Settings::getDebugKnobAt(15);
|
bool another_feature = Settings::GetDebugKnobAt(15);
|
||||||
```
|
```
|
||||||
|
|
||||||
```kts
|
```kts
|
||||||
//kotlin side
|
//kotlin side
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||||
|
|
||||||
|
//To use it as a general purpose uint var
|
||||||
|
val debug_knobs: Int = UShortSetting.DEBUG_KNOBS.getInt()
|
||||||
|
|
||||||
// Check if bit x is set
|
// Check if bit x is set
|
||||||
bool feature_enabled = Settings.getDebugKnobAt(x); //x as integer from 0 to 15
|
bool feature_enabled = Settings.GetDebugKnobAt(x); //x as integer from 0 to 15
|
||||||
```
|
```
|
||||||
|
|
||||||
The function returns `true` if the specified bit (0-15) is set in the `debug_knobs` value, `false` otherwise.
|
The function returns `true` if the specified bit (0-15) is set in the `debug_knobs` value, `false` otherwise.
|
||||||
@@ -247,6 +260,7 @@ There are two main confusions when talking about knobs:
|
|||||||
Sometimes when an user reports: knobs 1 and 2 gets better performance, dev may get confuse whether he means the knobs 1 and 2 literally, or the 1st and 2nd knobs (knobs 0 and 1).
|
Sometimes when an user reports: knobs 1 and 2 gets better performance, dev may get confuse whether he means the knobs 1 and 2 literally, or the 1st and 2nd knobs (knobs 0 and 1).
|
||||||
|
|
||||||
Debug knobs are **zero-based**, which means:
|
Debug knobs are **zero-based**, which means:
|
||||||
|
|
||||||
* The first knob is the knob(0) (or knob0 henceforth), and the last one is the 15 (knob15, likewise)
|
* The first knob is the knob(0) (or knob0 henceforth), and the last one is the 15 (knob15, likewise)
|
||||||
* You can talk: "knob0 is enabled/disabled", "In this video i was using only knobs 0 and 2", etc.
|
* You can talk: "knob0 is enabled/disabled", "In this video i was using only knobs 0 and 2", etc.
|
||||||
|
|
||||||
@@ -259,6 +273,7 @@ Whenever you're instructing tests or reporting results, be precise about whether
|
|||||||
|
|
||||||
ALWAYS use the word in PLURAL (knobs), without mentioning which one, to refer to the setting, aka multiple knobs at once:
|
ALWAYS use the word in PLURAL (knobs), without mentioning which one, to refer to the setting, aka multiple knobs at once:
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
- **knobs=0**: no knobs enabled
|
- **knobs=0**: no knobs enabled
|
||||||
- **knobs=1**: knob0 enabled, others disabled
|
- **knobs=1**: knob0 enabled, others disabled
|
||||||
- **knobs=2**: knob1 enabled, others disabled
|
- **knobs=2**: knob1 enabled, others disabled
|
||||||
@@ -270,6 +285,7 @@ Examples:
|
|||||||
|
|
||||||
Use the word in SINGULAR (knob), or in plural but referring which ones, when meaning multiple knobs at once:
|
Use the word in SINGULAR (knob), or in plural but referring which ones, when meaning multiple knobs at once:
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
- **knob0**: knob 0 enabled, others disabled
|
- **knob0**: knob 0 enabled, others disabled
|
||||||
- **knob1**: knob 1 enabled, others disabled
|
- **knob1**: knob 1 enabled, others disabled
|
||||||
- **knobs 0 and 1**: knobs 0 and 1 enabled, others disabled
|
- **knobs 0 and 1**: knobs 0 and 1 enabled, others disabled
|
||||||
@@ -282,12 +298,12 @@ Examples:
|
|||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void SomeFunction() {
|
void SomeFunction() {
|
||||||
if (Settings::getDebugKnobAt(0)) {
|
if (Settings::GetDebugKnobAt(0)) {
|
||||||
LOG_DEBUG(Common, "Debug feature 0 is enabled");
|
LOG_DEBUG(Common, "Debug feature 0 is enabled");
|
||||||
// Additional debug code here
|
// Additional debug code here
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Settings::getDebugKnobAt(1)) {
|
if (Settings::GetDebugKnobAt(1)) {
|
||||||
LOG_DEBUG(Common, "Debug feature 1 is enabled");
|
LOG_DEBUG(Common, "Debug feature 1 is enabled");
|
||||||
// Different debug behavior
|
// Different debug behavior
|
||||||
}
|
}
|
||||||
@@ -299,7 +315,7 @@ void SomeFunction() {
|
|||||||
```cpp
|
```cpp
|
||||||
bool UseOptimizedPath() {
|
bool UseOptimizedPath() {
|
||||||
// Skip optimization if debug bit 2 is set for testing
|
// Skip optimization if debug bit 2 is set for testing
|
||||||
return !Settings::getDebugKnobAt(2);
|
return !Settings::GetDebugKnobAt(2);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -309,7 +325,7 @@ bool UseOptimizedPath() {
|
|||||||
void ExperimentalFeature() {
|
void ExperimentalFeature() {
|
||||||
static constexpr u8 EXPERIMENTAL_FEATURE_BIT = 3;
|
static constexpr u8 EXPERIMENTAL_FEATURE_BIT = 3;
|
||||||
|
|
||||||
if (!Settings::getDebugKnobAt(EXPERIMENTAL_FEATURE_BIT)) {
|
if (!Settings::GetDebugKnobAt(EXPERIMENTAL_FEATURE_BIT)) {
|
||||||
// Fallback to stable implementation
|
// Fallback to stable implementation
|
||||||
StableImplementation();
|
StableImplementation();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ object NativeLibrary {
|
|||||||
|
|
||||||
external fun refreshThreadPolicies()
|
external fun refreshThreadPolicies()
|
||||||
|
|
||||||
external fun getDebugKnobAt(index: Int): Boolean
|
external fun GetDebugKnobAt(index: Int): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current speed limit to the configured turbo speed.
|
* Set the current speed limit to the configured turbo speed.
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ object Settings {
|
|||||||
fun getPlayerString(player: Int): String =
|
fun getPlayerString(player: Int): String =
|
||||||
YuzuApplication.appContext.getString(R.string.preferences_player, player)
|
YuzuApplication.appContext.getString(R.string.preferences_player, player)
|
||||||
|
|
||||||
fun getDebugKnobAt(index: Int): Boolean {
|
fun GetDebugKnobAt(index: Int): Boolean {
|
||||||
return org.yuzu.yuzu_emu.NativeLibrary.getDebugKnobAt(index)
|
return org.yuzu.yuzu_emu.NativeLibrary.GetDebugKnobAt(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
|
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
|
||||||
|
|||||||
+1
-2
@@ -11,8 +11,7 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
|
|||||||
enum class ShortSetting(override val key: String) : AbstractShortSetting {
|
enum class ShortSetting(override val key: String) : AbstractShortSetting {
|
||||||
RENDERER_SPEED_LIMIT("speed_limit"),
|
RENDERER_SPEED_LIMIT("speed_limit"),
|
||||||
RENDERER_TURBO_SPEED_LIMIT("turbo_speed_limit"),
|
RENDERER_TURBO_SPEED_LIMIT("turbo_speed_limit"),
|
||||||
RENDERER_SLOW_SPEED_LIMIT("slow_speed_limit"),
|
RENDERER_SLOW_SPEED_LIMIT("slow_speed_limit")
|
||||||
DEBUG_KNOBS("debug_knobs")
|
|
||||||
;
|
;
|
||||||
|
|
||||||
override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
|
override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features.settings.model
|
||||||
|
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
|
enum class UShortSetting(override val key: String) : AbstractIntSetting {
|
||||||
|
DEBUG_KNOBS("debug_knobs")
|
||||||
|
;
|
||||||
|
|
||||||
|
override fun getInt(needsGlobal: Boolean): Int =
|
||||||
|
NativeConfig.getUnsignedShort(key, needsGlobal)
|
||||||
|
|
||||||
|
override fun setInt(value: Int) {
|
||||||
|
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||||
|
global = false
|
||||||
|
}
|
||||||
|
NativeConfig.setUnsignedShort(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val defaultValue: Int by lazy { NativeConfig.getDefaultToString(key).toInt() }
|
||||||
|
|
||||||
|
override fun getValueAsString(needsGlobal: Boolean): String = getInt(needsGlobal).toString()
|
||||||
|
|
||||||
|
override fun reset() = NativeConfig.setUnsignedShort(key, defaultValue)
|
||||||
|
}
|
||||||
+2
-1
@@ -20,6 +20,7 @@ import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
|||||||
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
|
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
|
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.UShortSetting
|
||||||
import org.yuzu.yuzu_emu.network.NetDataValidators
|
import org.yuzu.yuzu_emu.network.NetDataValidators
|
||||||
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
@@ -1034,7 +1035,7 @@ abstract class SettingsItem(
|
|||||||
)
|
)
|
||||||
put(
|
put(
|
||||||
SpinBoxSetting(
|
SpinBoxSetting(
|
||||||
ShortSetting.DEBUG_KNOBS,
|
UShortSetting.DEBUG_KNOBS,
|
||||||
titleId = R.string.debug_knobs,
|
titleId = R.string.debug_knobs,
|
||||||
descriptionId = R.string.debug_knobs_description,
|
descriptionId = R.string.debug_knobs_description,
|
||||||
valueHint = R.string.debug_knobs_hint,
|
valueHint = R.string.debug_knobs_hint,
|
||||||
|
|||||||
+2
-1
@@ -25,6 +25,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings
|
|||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings.MenuTag
|
import org.yuzu.yuzu_emu.features.settings.model.Settings.MenuTag
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
|
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.UShortSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.view.*
|
import org.yuzu.yuzu_emu.features.settings.model.view.*
|
||||||
import org.yuzu.yuzu_emu.utils.InputHandler
|
import org.yuzu.yuzu_emu.utils.InputHandler
|
||||||
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
||||||
@@ -1326,7 +1327,7 @@ class SettingsFragmentPresenter(
|
|||||||
|
|
||||||
add(HeaderSetting(R.string.general))
|
add(HeaderSetting(R.string.general))
|
||||||
|
|
||||||
add(ShortSetting.DEBUG_KNOBS.key)
|
add(UShortSetting.DEBUG_KNOBS.key)
|
||||||
add(StringSetting.PROGRAM_ARGS.key)
|
add(StringSetting.PROGRAM_ARGS.key)
|
||||||
|
|
||||||
if (!NativeConfig.isPerGameConfigLoaded()) {
|
if (!NativeConfig.isPerGameConfigLoaded()) {
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ object NativeConfig {
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
external fun setShort(key: String, value: Short)
|
external fun setShort(key: String, value: Short)
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
external fun getUnsignedShort(key: String, needsGlobal: Boolean): Int
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
external fun setUnsignedShort(key: String, value: Int)
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
external fun getInt(key: String, needsGlobal: Boolean): Int
|
external fun getInt(key: String, needsGlobal: Boolean): Int
|
||||||
|
|
||||||
|
|||||||
@@ -1300,8 +1300,8 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_refreshThreadPolicies(JNIEnv* env, jo
|
|||||||
Common::RefreshThreadPolicies();
|
Common::RefreshThreadPolicies();
|
||||||
}
|
}
|
||||||
|
|
||||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_getDebugKnobAt(JNIEnv* env, jobject jobj, jint index) {
|
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDebugKnobAt(JNIEnv* env, jobject jobj, jint index) {
|
||||||
return static_cast<jboolean>(Settings::getDebugKnobAt(static_cast<u8>(index)));
|
return static_cast<jboolean>(Settings::GetDebugKnobAt(static_cast<u8>(index)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setTurboSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setTurboSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
|
||||||
|
|||||||
@@ -130,6 +130,25 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject ob
|
|||||||
setting->SetValue(value);
|
setting->SetValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getUnsignedShort(JNIEnv* env, jobject obj,
|
||||||
|
jstring jkey,
|
||||||
|
jboolean needGlobal) {
|
||||||
|
auto setting = getSetting<u16>(env, jkey);
|
||||||
|
if (setting == nullptr) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return static_cast<jint>(setting->GetValue(static_cast<bool>(needGlobal)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setUnsignedShort(JNIEnv* env, jobject obj,
|
||||||
|
jstring jkey, jint value) {
|
||||||
|
auto setting = getSetting<u16>(env, jkey);
|
||||||
|
if (setting == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setting->SetValue(static_cast<u16>(value));
|
||||||
|
}
|
||||||
|
|
||||||
jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey,
|
jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey,
|
||||||
jboolean needGlobal) {
|
jboolean needGlobal) {
|
||||||
auto setting = getSetting<int>(env, jkey);
|
auto setting = getSetting<int>(env, jkey);
|
||||||
|
|||||||
+20
-8
@@ -329,7 +329,7 @@ struct LogcatBackend : public Backend {
|
|||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
auto const df = GetDirectFormatArgs(entry);
|
auto const df = GetDirectFormatArgs(entry);
|
||||||
__android_log_print(android_log_priority, "YuzuNative", CCB_PRINTF_FMT, df.time_seconds, df.time_fractional, df.class_name, df.level_name, entry.filename, entry.line_num, entry.function, entry.message);
|
__android_log_print(android_log_priority, "YuzuNative", "%s %s:%u:%s: %s", df.class_name, entry.filename, entry.line_num, entry.function, entry.message);
|
||||||
}
|
}
|
||||||
void Flush() noexcept override {}
|
void Flush() noexcept override {}
|
||||||
};
|
};
|
||||||
@@ -428,21 +428,33 @@ void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename, u
|
|||||||
auto const flush = ::Settings::values.log_flush_line.GetValue();
|
auto const flush = ::Settings::values.log_flush_line.GetValue();
|
||||||
char buffer[BUFSIZ];
|
char buffer[BUFSIZ];
|
||||||
auto result = fmt::vformat_to_n(buffer, sizeof(buffer) - 1, format, args);
|
auto result = fmt::vformat_to_n(buffer, sizeof(buffer) - 1, format, args);
|
||||||
buffer[(std::min)(result.size, sizeof(buffer) - 1)] = '\0';
|
Entry e{
|
||||||
logging_instance->ForEachBackend([=](Backend& backend) {
|
.message = nullptr,
|
||||||
backend.Write(Entry{
|
.message_len = 0,
|
||||||
.message = buffer,
|
|
||||||
.message_len = (std::min)(result.size, sizeof(buffer) - 1),
|
|
||||||
.timestamp = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - logging_instance->time_origin),
|
.timestamp = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - logging_instance->time_origin),
|
||||||
.log_class = log_class,
|
.log_class = log_class,
|
||||||
.log_level = log_level,
|
.log_level = log_level,
|
||||||
.filename = TrimSourcePath(filename),
|
.filename = TrimSourcePath(filename),
|
||||||
.function = function,
|
.function = function,
|
||||||
.line_num = line_num,
|
.line_num = line_num,
|
||||||
|
};
|
||||||
|
if (result.size <= sizeof(buffer) - 1) {
|
||||||
|
buffer[(std::min)(result.size, sizeof(buffer) - 1)] = '\0';
|
||||||
|
e.message = buffer;
|
||||||
|
e.message_len = (std::min)(result.size, sizeof(buffer) - 1);
|
||||||
|
logging_instance->ForEachBackend([=](Backend& backend) {
|
||||||
|
backend.Write(e);
|
||||||
|
if (flush) backend.Flush();
|
||||||
});
|
});
|
||||||
if (flush)
|
} else {
|
||||||
backend.Flush();
|
std::string s = fmt::vformat(format, args);
|
||||||
|
e.message = s.c_str();
|
||||||
|
e.message_len = s.size();
|
||||||
|
logging_instance->ForEachBackend([=](Backend& backend) {
|
||||||
|
backend.Write(e);
|
||||||
|
if (flush) backend.Flush();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} // namespace Common::Log
|
} // namespace Common::Log
|
||||||
|
|||||||
@@ -126,9 +126,9 @@ void LogSettings() {
|
|||||||
setting->UsingGlobal() ? '-' : 'C', TranslateCategory(category),
|
setting->UsingGlobal() ? '-' : 'C', TranslateCategory(category),
|
||||||
setting->GetLabel());
|
setting->GetLabel());
|
||||||
if (is_default)
|
if (is_default)
|
||||||
settings_list.push_back(fmt::format("{}: {}\n", name, setting->Canonicalize()));
|
settings_list.push_back(fmt::format("{}: {}", name, setting->Canonicalize()));
|
||||||
else
|
else
|
||||||
settings_list.push_front(fmt::format("{}: {}\n", name, setting->Canonicalize()));
|
settings_list.push_front(fmt::format("{}: {}", name, setting->Canonicalize()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ void LogSettings() {
|
|||||||
#undef LOG_PATH
|
#undef LOG_PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
bool getDebugKnobAt(u8 i) {
|
bool GetDebugKnobAt(u8 i) {
|
||||||
return (values.debug_knobs.GetValue() & (1 << (i & 0xF))) != 0;
|
return (values.debug_knobs.GetValue() & (1 << (i & 0xF))) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -904,7 +904,7 @@ struct Values {
|
|||||||
0,
|
0,
|
||||||
65535,
|
65535,
|
||||||
"debug_knobs",
|
"debug_knobs",
|
||||||
Category::Debugging,
|
Category::System,
|
||||||
Specialization::Countable,
|
Specialization::Countable,
|
||||||
true,
|
true,
|
||||||
true};
|
true};
|
||||||
@@ -947,7 +947,7 @@ constexpr u32 MAX_FRAME_GEN_MULTIPLIER = 4;
|
|||||||
|
|
||||||
[[nodiscard]] size_t FrameGenMaxGenerations();
|
[[nodiscard]] size_t FrameGenMaxGenerations();
|
||||||
|
|
||||||
bool getDebugKnobAt(u8 i);
|
bool GetDebugKnobAt(u8 i);
|
||||||
|
|
||||||
void UpdateGPUAccuracy();
|
void UpdateGPUAccuracy();
|
||||||
bool IsGPULevelHigh();
|
bool IsGPULevelHigh();
|
||||||
|
|||||||
+185
-232
@@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <map>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <span>
|
||||||
|
#include <cctype>
|
||||||
|
#include <ankerl/unordered_dense.h>
|
||||||
|
|
||||||
#include "common/hex_util.h"
|
#include "common/hex_util.h"
|
||||||
#include "common/logging.h"
|
#include "common/logging.h"
|
||||||
@@ -22,61 +24,30 @@ enum class IPSFileType {
|
|||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr std::array<std::pair<const char*, const char*>, 11> ESCAPE_CHARACTER_MAP{{
|
static IPSFileType IdentifyMagic(std::span<const u8> magic) {
|
||||||
{"\\a", "\a"},
|
if (magic.size() >= 5) {
|
||||||
{"\\b", "\b"},
|
if (std::memcmp(magic.data(), "PATCH", 5) == 0)
|
||||||
{"\\f", "\f"},
|
|
||||||
{"\\n", "\n"},
|
|
||||||
{"\\r", "\r"},
|
|
||||||
{"\\t", "\t"},
|
|
||||||
{"\\v", "\v"},
|
|
||||||
{"\\\\", "\\"},
|
|
||||||
{"\\\'", "\'"},
|
|
||||||
{"\\\"", "\""},
|
|
||||||
{"\\\?", "\?"},
|
|
||||||
}};
|
|
||||||
|
|
||||||
static IPSFileType IdentifyMagic(const std::vector<u8>& magic) {
|
|
||||||
if (magic.size() != 5) {
|
|
||||||
return IPSFileType::Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
static constexpr std::array<u8, 5> patch_magic{{'P', 'A', 'T', 'C', 'H'}};
|
|
||||||
if (std::equal(magic.begin(), magic.end(), patch_magic.begin())) {
|
|
||||||
return IPSFileType::IPS;
|
return IPSFileType::IPS;
|
||||||
}
|
if (std::memcmp(magic.data(), "IPS32", 5) == 0)
|
||||||
|
|
||||||
static constexpr std::array<u8, 5> ips32_magic{{'I', 'P', 'S', '3', '2'}};
|
|
||||||
if (std::equal(magic.begin(), magic.end(), ips32_magic.begin())) {
|
|
||||||
return IPSFileType::IPS32;
|
return IPSFileType::IPS32;
|
||||||
}
|
}
|
||||||
|
|
||||||
return IPSFileType::Error;
|
return IPSFileType::Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsEOF(IPSFileType type, const std::vector<u8>& data) {
|
static bool IsEOF(IPSFileType type, std::span<const u8> magic) {
|
||||||
static constexpr std::array<u8, 3> eof{{'E', 'O', 'F'}};
|
return (type == IPSFileType::IPS && magic.size() > 3 && std::memcmp(magic.data(), "EOF", 3) == 0)
|
||||||
if (type == IPSFileType::IPS && std::equal(data.begin(), data.end(), eof.begin())) {
|
|| (type == IPSFileType::IPS32 && magic.size() > 4 && std::memcmp(magic.data(), "EEOF", 4) == 0);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static constexpr std::array<u8, 4> eeof{{'E', 'E', 'O', 'F'}};
|
|
||||||
return type == IPSFileType::IPS32 && std::equal(data.begin(), data.end(), eeof.begin());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) {
|
VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) {
|
||||||
if (in == nullptr || ips == nullptr)
|
if (in == nullptr || ips == nullptr)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
const auto type = IdentifyMagic(ips->ReadBytes(0x5));
|
auto in_data = in->ReadAllBytes();
|
||||||
|
auto const type = IdentifyMagic(in_data);
|
||||||
if (type == IPSFileType::Error)
|
if (type == IPSFileType::Error)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
auto in_data = in->ReadAllBytes();
|
|
||||||
if (in_data.size() == 0) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<u8> temp(type == IPSFileType::IPS ? 3 : 4);
|
std::vector<u8> temp(type == IPSFileType::IPS ? 3 : 4);
|
||||||
u64 offset = 5; // After header
|
u64 offset = 5; // After header
|
||||||
while (ips->Read(temp.data(), temp.size(), offset) == temp.size()) {
|
while (ips->Read(temp.data(), temp.size(), offset) == temp.size()) {
|
||||||
@@ -85,12 +56,9 @@ VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 real_offset{};
|
u32 real_offset = (type == IPSFileType::IPS32)
|
||||||
if (type == IPSFileType::IPS32)
|
? ((temp[0] << 24) | (temp[1] << 16) | (temp[2] << 8) | temp[3])
|
||||||
real_offset = (temp[0] << 24) | (temp[1] << 16) | (temp[2] << 8) | temp[3];
|
: ((temp[0] << 16) | (temp[1] << 8) | temp[2]);
|
||||||
else
|
|
||||||
real_offset = (temp[0] << 16) | (temp[1] << 8) | temp[2];
|
|
||||||
|
|
||||||
if (real_offset > in_data.size()) {
|
if (real_offset > in_data.size()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -113,34 +81,35 @@ VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
if (real_offset + rle_size > in_data.size())
|
if (real_offset + rle_size > in_data.size())
|
||||||
rle_size = static_cast<u16>(in_data.size() - real_offset);
|
rle_size = u16(in_data.size() - real_offset);
|
||||||
std::memset(in_data.data() + real_offset, *data, rle_size);
|
std::memset(in_data.data() + real_offset, *data, rle_size);
|
||||||
} else { // Standard Patch
|
} else { // Standard Patch
|
||||||
auto read = data_size;
|
auto read = data_size;
|
||||||
if (real_offset + read > in_data.size())
|
if (real_offset + read > in_data.size())
|
||||||
read = static_cast<u16>(in_data.size() - real_offset);
|
read = u16(in_data.size() - real_offset);
|
||||||
if (ips->Read(in_data.data() + real_offset, read, offset) != data_size)
|
if (ips->Read(in_data.data() + real_offset, read, offset) != data_size)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
offset += data_size;
|
offset += data_size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (IsEOF(type, temp)) {
|
||||||
if (!IsEOF(type, temp)) {
|
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(), in->GetContainingDirectory());
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
return nullptr;
|
||||||
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(),
|
|
||||||
in->GetContainingDirectory());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct IPSwitchRecord {
|
||||||
|
std::array<uint8_t, 256 - sizeof(size_t)> data;
|
||||||
|
size_t count;
|
||||||
|
};
|
||||||
struct IPSwitchCompiler::IPSwitchPatch {
|
struct IPSwitchCompiler::IPSwitchPatch {
|
||||||
std::string name;
|
ankerl::unordered_dense::map<u32, IPSwitchRecord> records;
|
||||||
bool enabled;
|
bool enabled;
|
||||||
std::map<u32, std::vector<u8>> records;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
IPSwitchCompiler::IPSwitchCompiler(VirtualFile patch_text_) : patch_text(std::move(patch_text_)) {
|
IPSwitchCompiler::IPSwitchCompiler(VirtualFile patch_text_) : patch_text(std::move(patch_text_)) {
|
||||||
Parse();
|
Parse(patch_text->ReadAllBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
IPSwitchCompiler::~IPSwitchCompiler() = default;
|
IPSwitchCompiler::~IPSwitchCompiler() = default;
|
||||||
@@ -149,201 +118,185 @@ std::array<u8, 32> IPSwitchCompiler::GetBuildID() const {
|
|||||||
return nso_build_id;
|
return nso_build_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IPSwitchCompiler::IsValid() const {
|
static IPSwitchRecord EscapeStringSequences(std::string_view sv) {
|
||||||
return valid;
|
IPSwitchRecord r{};
|
||||||
}
|
for (auto it = sv.cbegin(); it != sv.cend(); ) {
|
||||||
|
if (*it == '\\' && it + 1 < sv.cend()) {
|
||||||
static bool StartsWith(std::string_view base, std::string_view check) {
|
switch (it[1]) {
|
||||||
return base.size() >= check.size() && base.substr(0, check.size()) == check;
|
case 'a': r.data[r.count] = '\a'; break;
|
||||||
}
|
case 'b': r.data[r.count] = '\b'; break;
|
||||||
|
case 'e': r.data[r.count] = '\e'; break;
|
||||||
static std::string EscapeStringSequences(std::string in) {
|
case 'f': r.data[r.count] = '\f'; break;
|
||||||
for (const auto& seq : ESCAPE_CHARACTER_MAP) {
|
case 'n': r.data[r.count] = '\n'; break;
|
||||||
for (auto index = in.find(seq.first); index != std::string::npos;
|
case 'r': r.data[r.count] = '\r'; break;
|
||||||
index = in.find(seq.first, index)) {
|
case 't': r.data[r.count] = '\t'; break;
|
||||||
in.replace(index, std::strlen(seq.first), seq.second);
|
case 'v': r.data[r.count] = '\v'; break;
|
||||||
index += std::strlen(seq.second);
|
case '?': r.data[r.count] = '\?'; break;
|
||||||
|
default: r.data[r.count] = it[1]; break;
|
||||||
}
|
}
|
||||||
}
|
++r.count;
|
||||||
|
it += 2;
|
||||||
return in;
|
|
||||||
}
|
|
||||||
|
|
||||||
void IPSwitchCompiler::ParseFlag(const std::string& line) {
|
|
||||||
if (StartsWith(line, "@flag offset_shift ")) {
|
|
||||||
// Offset Shift Flag
|
|
||||||
offset_shift = std::strtoll(line.substr(19).c_str(), nullptr, 0);
|
|
||||||
} else if (StartsWith(line, "@little-endian")) {
|
|
||||||
// Set values to read as little endian
|
|
||||||
is_little_endian = true;
|
|
||||||
} else if (StartsWith(line, "@big-endian")) {
|
|
||||||
// Set values to read as big endian
|
|
||||||
is_little_endian = false;
|
|
||||||
} else if (StartsWith(line, "@flag print_values")) {
|
|
||||||
// Force printing of applied values
|
|
||||||
print_values = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void IPSwitchCompiler::Parse() {
|
|
||||||
const auto bytes = patch_text->ReadAllBytes();
|
|
||||||
std::stringstream s;
|
|
||||||
s.write(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
|
||||||
|
|
||||||
std::vector<std::string> lines;
|
|
||||||
std::string stream_line;
|
|
||||||
while (std::getline(s, stream_line)) {
|
|
||||||
// Remove a trailing \r
|
|
||||||
if (!stream_line.empty() && stream_line.back() == '\r')
|
|
||||||
stream_line.pop_back();
|
|
||||||
lines.push_back(std::move(stream_line));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < lines.size(); ++i) {
|
|
||||||
auto line = lines[i];
|
|
||||||
|
|
||||||
// Remove midline comments
|
|
||||||
std::size_t comment_index = std::string::npos;
|
|
||||||
bool within_string = false;
|
|
||||||
for (std::size_t k = 0; k < line.size(); ++k) {
|
|
||||||
if (line[k] == '\"' && (k > 0 && line[k - 1] != '\\')) {
|
|
||||||
within_string = !within_string;
|
|
||||||
} else if (line[k] == '\\' && (k < line.size() - 1 && line[k + 1] == '\\')) {
|
|
||||||
comment_index = k;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StartsWith(line, "//") && comment_index != std::string::npos) {
|
|
||||||
last_comment = line.substr(comment_index + 2);
|
|
||||||
line = line.substr(0, comment_index);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StartsWith(line, "@stop")) {
|
|
||||||
// Force stop
|
|
||||||
break;
|
|
||||||
} else if (StartsWith(line, "@nsobid-")) {
|
|
||||||
// NSO Build ID Specifier
|
|
||||||
const auto raw_build_id = fmt::format("{:0<64}", line.substr(8));
|
|
||||||
nso_build_id = Common::HexStringToArray<0x20>(raw_build_id);
|
|
||||||
} else if (StartsWith(line, "#")) {
|
|
||||||
// Mandatory Comment
|
|
||||||
LOG_INFO(Loader, "[IPSwitchCompiler ('{}')] Forced output comment: {}",
|
|
||||||
patch_text->GetName(), line.substr(1));
|
|
||||||
} else if (StartsWith(line, "//")) {
|
|
||||||
// Normal Comment
|
|
||||||
last_comment = line.substr(2);
|
|
||||||
if (last_comment.find_first_not_of(' ') == std::string::npos)
|
|
||||||
continue;
|
|
||||||
if (last_comment.find_first_not_of(' ') != 0)
|
|
||||||
last_comment = last_comment.substr(last_comment.find_first_not_of(' '));
|
|
||||||
} else if (StartsWith(line, "@enabled") || StartsWith(line, "@disabled")) {
|
|
||||||
// Start of patch
|
|
||||||
const auto enabled = StartsWith(line, "@enabled");
|
|
||||||
if (i == 0)
|
|
||||||
return;
|
|
||||||
LOG_INFO(Loader, "[IPSwitchCompiler ('{}')] Parsing patch '{}' ({})",
|
|
||||||
patch_text->GetName(), last_comment, line.substr(1));
|
|
||||||
|
|
||||||
IPSwitchPatch patch{last_comment, enabled, {}};
|
|
||||||
|
|
||||||
// Read rest of patch
|
|
||||||
while (true) {
|
|
||||||
if (i + 1 >= lines.size()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& patch_line = lines[++i];
|
|
||||||
|
|
||||||
// Patch line may contain comments
|
|
||||||
if (StartsWith(patch_line, "//") || StartsWith(patch_line, "#")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start of new patch
|
|
||||||
if (StartsWith(patch_line, "@enabled") || StartsWith(patch_line, "@disabled")) {
|
|
||||||
--i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a flag
|
|
||||||
if (StartsWith(patch_line, "@")) {
|
|
||||||
ParseFlag(patch_line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 11 - 8 hex digit offset + space + minimum two digit overwrite val
|
|
||||||
if (patch_line.length() < 11)
|
|
||||||
break;
|
|
||||||
auto offset = std::strtoul(patch_line.substr(0, 8).c_str(), nullptr, 16);
|
|
||||||
offset += static_cast<unsigned long>(offset_shift);
|
|
||||||
|
|
||||||
std::vector<u8> replace;
|
|
||||||
// 9 - first char of replacement val
|
|
||||||
if (patch_line[9] == '\"') {
|
|
||||||
// string replacement
|
|
||||||
auto end_index = patch_line.find('\"', 10);
|
|
||||||
if (end_index == std::string::npos || end_index < 10)
|
|
||||||
return;
|
|
||||||
while (patch_line[end_index - 1] == '\\') {
|
|
||||||
end_index = patch_line.find('\"', end_index + 1);
|
|
||||||
if (end_index == std::string::npos || end_index < 10)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto value = patch_line.substr(10, end_index - 10);
|
|
||||||
value = EscapeStringSequences(value);
|
|
||||||
replace.reserve(value.size());
|
|
||||||
std::copy(value.begin(), value.end(), std::back_inserter(replace));
|
|
||||||
} else {
|
} else {
|
||||||
// hex replacement
|
++r.count;
|
||||||
const auto value =
|
++it;
|
||||||
patch_line.substr(9, patch_line.find_first_of(" /\r\n", 9) - 9);
|
|
||||||
replace = Common::HexStringToVector(value, is_little_endian);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
if (print_values) {
|
void IPSwitchCompiler::Parse(std::span<u8 const> bytes) {
|
||||||
LOG_INFO(Loader,
|
LOG_INFO(Loader, "IPSwitchCompiler: '{}'", patch_text->GetName());
|
||||||
"[IPSwitchCompiler ('{}')] - Patching value at offset {:#08x} "
|
bool is_little_endian = true;
|
||||||
"with byte string '{}'",
|
s64 offset_shift = 0;
|
||||||
patch_text->GetName(), offset, Common::HexToString(replace));
|
//bool print_values = false;
|
||||||
|
auto const parse_line = [&](std::string_view const line) {
|
||||||
|
// Keep in mind lines have trimmed spaces (at the end & start)!
|
||||||
|
LOG_INFO(Loader, "<{}>", line);
|
||||||
|
// IPSwitch is case insensitive
|
||||||
|
// Yes this is how the logic goes for the main reference parsers!
|
||||||
|
if (line.size() > 2 && line[0] == '@') {
|
||||||
|
switch (line[1]) {
|
||||||
|
// yes, @nsobid too -- NSO Build ID Specifier
|
||||||
|
case 'n':
|
||||||
|
case 'N':
|
||||||
|
nso_build_id = Common::HexStringToArray<0x20>(fmt::format("{:0<64}", line.substr(8)));
|
||||||
|
break;
|
||||||
|
// @stop
|
||||||
|
case 's':
|
||||||
|
case 'S':
|
||||||
|
return false;
|
||||||
|
// @enabled
|
||||||
|
case 'e':
|
||||||
|
case 'E':
|
||||||
|
patches.push_back({{}, true});
|
||||||
|
break;
|
||||||
|
// @disabled
|
||||||
|
case 'd':
|
||||||
|
case 'D':
|
||||||
|
patches.push_back({{}, false});
|
||||||
|
break;
|
||||||
|
// @flag
|
||||||
|
case 'f':
|
||||||
|
case 'F': {
|
||||||
|
if (line.starts_with("@flag offset_shift")) {
|
||||||
|
offset_shift = std::strtoll(line.data() + 19, nullptr, 0); // Offset Shift Flag
|
||||||
|
} else if (line.starts_with("@flag print_values")) {
|
||||||
|
//print_values = true; // Force printing of applied values
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'l':
|
||||||
|
case 'L':
|
||||||
|
is_little_endian = true;
|
||||||
|
break;
|
||||||
|
// IPS parsers dont support big endian no more, we do due to backcompat
|
||||||
|
case 'b':
|
||||||
|
case 'B':
|
||||||
|
is_little_endian = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG_WARNING(Loader, "Unknown flag {}", line);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
size_t offset = size_t(std::strtoul(line.data(), nullptr, 16));
|
||||||
|
offset += size_t(offset_shift);
|
||||||
|
if (auto const first_quote = line.find_first_of("\"\'"); first_quote != std::string::npos) {
|
||||||
|
// string replacement
|
||||||
|
char quote = line[first_quote];
|
||||||
|
auto const start = line.cbegin() + first_quote + 1;
|
||||||
|
auto end = start;
|
||||||
|
for (; end < line.cend() && *end != quote; )
|
||||||
|
end += (*end == '\\') ? 2 : 1;
|
||||||
|
if (start <= line.cend() && end <= line.cend()) {
|
||||||
|
LOG_INFO(Loader, "[S] value @ {:#08X} ", offset);
|
||||||
|
patches.back().records.insert_or_assign(u32(offset), EscapeStringSequences({start, end}));
|
||||||
|
} else {
|
||||||
|
LOG_WARNING(Loader, "invalid string");
|
||||||
|
}
|
||||||
|
} else if (auto const first_space = line.find_last_of(" /\t\r\n"); first_space != std::string::npos) {
|
||||||
|
IPSwitchRecord r{}; // hex replacement
|
||||||
|
auto const start = line.cbegin() + first_space + 1;
|
||||||
|
auto const end = line.cend();
|
||||||
|
if (start <= line.cend() && end <= line.cend()) {
|
||||||
|
// Actually IPS wants ordering from {lsb, ..., msb} -- so LE and BE are inverted, fun!
|
||||||
|
auto const hs = Common::HexStringToVector({start, end}, is_little_endian);
|
||||||
|
std::memcpy(r.data.data(), hs.data(), hs.size());
|
||||||
|
r.count = hs.size();
|
||||||
|
LOG_INFO(Loader, "[H] value @ {:#08X}", offset);
|
||||||
|
patches.back().records.insert_or_assign(u32(offset), std::move(r));
|
||||||
|
} else {
|
||||||
|
LOG_WARNING(Loader, "invalid line");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARNING(Loader, "unhandled line!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; //continue
|
||||||
|
};
|
||||||
|
|
||||||
patch.records.insert_or_assign(static_cast<u32>(offset), std::move(replace));
|
for (auto it = bytes.begin(); it < bytes.end(); ) {
|
||||||
}
|
auto const start = it;
|
||||||
|
auto end = start;
|
||||||
patches.push_back(std::move(patch));
|
for (; end < bytes.end() && *end != '\n' && *end != '\r'; ++end)
|
||||||
} else if (StartsWith(line, "@")) {
|
;
|
||||||
ParseFlag(line);
|
it = end + 1; //prepare for next line
|
||||||
|
std::string_view const sline{
|
||||||
|
reinterpret_cast<const char*>(bytes.data() + std::distance(bytes.begin(), start)),
|
||||||
|
size_t(std::distance(start, end))
|
||||||
|
};
|
||||||
|
if (sline.size() > 0) {
|
||||||
|
auto p = sline.cbegin();
|
||||||
|
// skip space off line
|
||||||
|
for (; p < sline.cend() && std::isspace(*p); ++p)
|
||||||
|
;
|
||||||
|
// now make a nominal preprocessed line: remove comments
|
||||||
|
char quote = '\0';
|
||||||
|
auto const sline_start = p;
|
||||||
|
for (; p < sline.cend(); ) {
|
||||||
|
// we dont check for "//", IPS checks for '/' only...
|
||||||
|
if ((!quote && p[0] == '/')
|
||||||
|
|| (!quote && p[0] == '#')) {
|
||||||
|
break;
|
||||||
|
} else if (p[0] == '\"' || p[0] == '\'') {
|
||||||
|
quote = (p[0] == quote) ? '\0' : p[0];
|
||||||
|
++p;
|
||||||
|
} else if (p + 1 < sline.cend() && p[0] == '\\') {
|
||||||
|
p += 2;
|
||||||
|
} else {
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now we have the preprocessed string ;)
|
||||||
|
std::string_view pp_str(sline_start, p);
|
||||||
|
if (pp_str.size() > 0 && !parse_line(pp_str)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
valid = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VirtualFile IPSwitchCompiler::Apply(const VirtualFile& in) const {
|
VirtualFile IPSwitchCompiler::Apply(const VirtualFile& in) const {
|
||||||
if (in == nullptr || !valid)
|
if (in == nullptr)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
auto in_data = in->ReadAllBytes();
|
auto in_data = in->ReadAllBytes();
|
||||||
|
|
||||||
for (const auto& patch : patches) {
|
for (const auto& patch : patches) {
|
||||||
if (!patch.enabled)
|
if (patch.enabled) {
|
||||||
continue;
|
|
||||||
|
|
||||||
for (const auto& record : patch.records) {
|
for (const auto& record : patch.records) {
|
||||||
if (record.first >= in_data.size())
|
if (record.first < in_data.size()) {
|
||||||
continue;
|
auto replace_size = record.second.count;
|
||||||
auto replace_size = record.second.size();
|
|
||||||
if (record.first + replace_size > in_data.size())
|
if (record.first + replace_size > in_data.size())
|
||||||
replace_size = in_data.size() - record.first;
|
replace_size = in_data.size() - record.first;
|
||||||
for (std::size_t i = 0; i < replace_size; ++i)
|
std::memcpy(in_data.data() + record.first, record.second.data.data(), replace_size);
|
||||||
in_data[i + record.first] = record.second[i];
|
} else {
|
||||||
|
LOG_WARNING(Loader, "record offs={:x},size={:x}", record.first, record.second.data.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(),
|
}
|
||||||
in->GetContainingDirectory());
|
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(), in->GetContainingDirectory());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace FileSys
|
} // namespace FileSys
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "core/file_sys/vfs/vfs.h"
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
@@ -20,24 +23,17 @@ public:
|
|||||||
~IPSwitchCompiler();
|
~IPSwitchCompiler();
|
||||||
|
|
||||||
std::array<u8, 0x20> GetBuildID() const;
|
std::array<u8, 0x20> GetBuildID() const;
|
||||||
bool IsValid() const;
|
|
||||||
VirtualFile Apply(const VirtualFile& in) const;
|
VirtualFile Apply(const VirtualFile& in) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct IPSwitchPatch;
|
struct IPSwitchPatch;
|
||||||
|
|
||||||
void ParseFlag(const std::string& flag);
|
void ParseFlag(const std::string& flag);
|
||||||
void Parse();
|
void Parse(std::span<u8 const> bytes);
|
||||||
|
|
||||||
bool valid = false;
|
|
||||||
|
|
||||||
VirtualFile patch_text;
|
VirtualFile patch_text;
|
||||||
std::vector<IPSwitchPatch> patches;
|
std::vector<IPSwitchPatch> patches;
|
||||||
std::array<u8, 0x20> nso_build_id{};
|
std::array<u8, 0x20> nso_build_id{};
|
||||||
bool is_little_endian = false;
|
|
||||||
s64 offset_shift = 0;
|
|
||||||
bool print_values = false;
|
|
||||||
std::string last_comment = "";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace FileSys
|
} // namespace FileSys
|
||||||
|
|||||||
@@ -345,8 +345,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
|
|||||||
return exefs;
|
return exefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs,
|
std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs, const std::string& build_id) const {
|
||||||
const std::string& build_id) const {
|
|
||||||
const auto& disabled = Settings::values.disabled_addons[title_id];
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||||
const auto nso_build_id = fmt::format("{:0<64}", build_id);
|
const auto nso_build_id = fmt::format("{:0<64}", build_id);
|
||||||
|
|
||||||
@@ -361,16 +360,11 @@ std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualD
|
|||||||
for (const auto& file : exefs_dir->GetFiles()) {
|
for (const auto& file : exefs_dir->GetFiles()) {
|
||||||
if (file->GetExtension() == "ips") {
|
if (file->GetExtension() == "ips") {
|
||||||
auto name = file->GetName();
|
auto name = file->GetName();
|
||||||
|
const auto this_build_id = fmt::format("{:0<64}", name.substr(0, name.find('.')));
|
||||||
const auto this_build_id =
|
|
||||||
fmt::format("{:0<64}", name.substr(0, name.find('.')));
|
|
||||||
if (nso_build_id == this_build_id)
|
if (nso_build_id == this_build_id)
|
||||||
out.push_back(file);
|
out.push_back(file);
|
||||||
} else if (file->GetExtension() == "pchtxt") {
|
} else if (file->GetExtension() == "pchtxt") {
|
||||||
IPSwitchCompiler compiler{file};
|
IPSwitchCompiler compiler{file};
|
||||||
if (!compiler.IsValid())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const auto this_build_id = Common::HexToString(compiler.GetBuildID());
|
const auto this_build_id = Common::HexToString(compiler.GetBuildID());
|
||||||
if (nso_build_id == this_build_id)
|
if (nso_build_id == this_build_id)
|
||||||
out.push_back(file);
|
out.push_back(file);
|
||||||
@@ -378,7 +372,6 @@ std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,56 +17,12 @@
|
|||||||
|
|
||||||
namespace Kernel::Svc {
|
namespace Kernel::Svc {
|
||||||
|
|
||||||
constexpr auto MAX_MSG_TIME = std::chrono::milliseconds(250);
|
|
||||||
const auto MAX_MSG_SIZE = 0x1000;
|
|
||||||
|
|
||||||
/// Used to output a message on a debug hardware unit - does nothing on a retail unit
|
/// Used to output a message on a debug hardware unit - does nothing on a retail unit
|
||||||
Result OutputDebugString(Core::System& system, u64 address, u64 len) {
|
Result OutputDebugString(Core::System& system, u64 address, u64 len) {
|
||||||
static struct DebugFlusher {
|
|
||||||
std::string msg_buffer;
|
|
||||||
std::mutex msg_mutex;
|
|
||||||
std::condition_variable msg_cv;
|
|
||||||
std::chrono::steady_clock::time_point last_msg_time;
|
|
||||||
std::optional<std::jthread> thread;
|
|
||||||
} flusher_data;
|
|
||||||
R_SUCCEED_IF(len == 0);
|
R_SUCCEED_IF(len == 0);
|
||||||
// Only start the thread the very first time this function is called
|
std::string msg_buffer(len, 0);
|
||||||
if (!flusher_data.thread) {
|
GetCurrentMemory(system.Kernel()).ReadBlock(address, msg_buffer.data(), len);
|
||||||
flusher_data.thread.emplace([](std::stop_token stop_token) {
|
LOG_INFO(Debug_Emulated, "{}", msg_buffer);
|
||||||
while (!stop_token.stop_requested()) {
|
|
||||||
std::unique_lock lock(flusher_data.msg_mutex);
|
|
||||||
flusher_data.msg_cv.wait(lock, [&stop_token] {
|
|
||||||
return !flusher_data.msg_buffer.empty() || stop_token.stop_requested();
|
|
||||||
});
|
|
||||||
if (stop_token.stop_requested() && flusher_data.msg_buffer.empty())
|
|
||||||
break;
|
|
||||||
auto timeout = flusher_data.last_msg_time + MAX_MSG_TIME;
|
|
||||||
bool woke_early = flusher_data.msg_cv.wait_until(lock, timeout, [&stop_token] {
|
|
||||||
return flusher_data.msg_buffer.size() >= MAX_MSG_SIZE || stop_token.stop_requested();
|
|
||||||
});
|
|
||||||
if (!woke_early || flusher_data.msg_buffer.size() >= MAX_MSG_SIZE || stop_token.stop_requested()) {
|
|
||||||
if (!flusher_data.msg_buffer.empty()) {
|
|
||||||
// Remove trailing newline as LOG_INFO adds that anyways
|
|
||||||
if (flusher_data.msg_buffer.back() == '\n')
|
|
||||||
flusher_data.msg_buffer.pop_back();
|
|
||||||
|
|
||||||
LOG_INFO(Debug_Emulated, "\n{}", flusher_data.msg_buffer);
|
|
||||||
flusher_data.msg_buffer.clear();
|
|
||||||
}
|
|
||||||
if (stop_token.stop_requested()) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flusher_data.msg_cv.notify_all();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
{
|
|
||||||
std::lock_guard lock(flusher_data.msg_mutex);
|
|
||||||
const auto old_size = flusher_data.msg_buffer.size();
|
|
||||||
flusher_data.msg_buffer.resize(old_size + len);
|
|
||||||
GetCurrentMemory(system.Kernel()).ReadBlock(address, flusher_data.msg_buffer.data() + old_size, len);
|
|
||||||
flusher_data.last_msg_time = std::chrono::steady_clock::now();
|
|
||||||
}
|
|
||||||
flusher_data.msg_cv.notify_one();
|
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <system_error>
|
||||||
#include <JlCompress.h>
|
#include <JlCompress.h>
|
||||||
#include "frontend_common/mod_manager.h"
|
#include "frontend_common/mod_manager.h"
|
||||||
#include "mod.h"
|
#include "mod.h"
|
||||||
|
|||||||
@@ -440,7 +440,8 @@ void SetupCapabilities(const Profile& profile, const Info& info, EmitContext& ct
|
|||||||
}
|
}
|
||||||
if ((info.uses_subgroup_vote || info.uses_subgroup_invocation_id ||
|
if ((info.uses_subgroup_vote || info.uses_subgroup_invocation_id ||
|
||||||
info.uses_subgroup_shuffles) &&
|
info.uses_subgroup_shuffles) &&
|
||||||
profile.support_vote && profile.SupportsSubgroupStage(ctx.stage)) {
|
profile.support_vote &&
|
||||||
|
(ctx.stage != Stage::Geometry || profile.support_subgroup_in_geometry_stage)) {
|
||||||
ctx.AddCapability(spv::Capability::GroupNonUniformBallot);
|
ctx.AddCapability(spv::Capability::GroupNonUniformBallot);
|
||||||
ctx.AddCapability(spv::Capability::GroupNonUniformShuffle);
|
ctx.AddCapability(spv::Capability::GroupNonUniformShuffle);
|
||||||
if (!profile.warp_size_potentially_larger_than_guest) {
|
if (!profile.warp_size_potentially_larger_than_guest) {
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ Id SubgroupScope(EmitContext& ctx) {
|
|||||||
return ctx.Const(static_cast<u32>(spv::Scope::Subgroup));
|
return ctx.Const(static_cast<u32>(spv::Scope::Subgroup));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some mobile GPUs (e.g. Adreno/Turnip) only advertise subgroup ballot/shuffle support for the
|
||||||
|
// fragment and compute stages (VkPhysicalDeviceSubgroupProperties::supportedStages), even though
|
||||||
|
// they support these operations elsewhere. Guest shaders that use VOTE/SHFL in a geometry program
|
||||||
|
// would otherwise emit GroupNonUniform* SPIR-V the driver never declared support for in that
|
||||||
|
// stage. There is no barrier in the geometry stage, so a real cross-invocation emulation can't be
|
||||||
|
// made correct; instead, treat the current invocation as if it were alone in its subgroup. This is
|
||||||
|
// semantically wrong for guest code that relies on genuine cross-lane communication, but it is
|
||||||
|
// well-defined, valid SPIR-V that doesn't depend on unsupported hardware capabilities.
|
||||||
|
bool NeedsGeometrySubgroupFallback(EmitContext& ctx) {
|
||||||
|
return ctx.stage == Stage::Geometry && !ctx.profile.support_subgroup_in_geometry_stage;
|
||||||
|
}
|
||||||
|
|
||||||
bool StageSupportsSubgroups(EmitContext& ctx) {
|
bool StageSupportsSubgroups(EmitContext& ctx) {
|
||||||
return ctx.profile.SupportsSubgroupStage(ctx.stage);
|
return ctx.profile.SupportsSubgroupStage(ctx.stage);
|
||||||
}
|
}
|
||||||
@@ -94,6 +106,9 @@ Id AddPartitionBase(EmitContext& ctx, Id thread_id) {
|
|||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
||||||
Id EmitLaneId(EmitContext& ctx) {
|
Id EmitLaneId(EmitContext& ctx) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
return ctx.u32_zero_value;
|
||||||
|
}
|
||||||
const Id id{GetThreadId(ctx)};
|
const Id id{GetThreadId(ctx)};
|
||||||
if (!ctx.profile.warp_size_potentially_larger_than_guest) {
|
if (!ctx.profile.warp_size_potentially_larger_than_guest) {
|
||||||
return id;
|
return id;
|
||||||
@@ -102,6 +117,9 @@ Id EmitLaneId(EmitContext& ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Id EmitVoteAll(EmitContext& ctx, Id pred) {
|
Id EmitVoteAll(EmitContext& ctx, Id pred) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
return pred;
|
||||||
|
}
|
||||||
if (!StageSupportsSubgroups(ctx)) {
|
if (!StageSupportsSubgroups(ctx)) {
|
||||||
return pred;
|
return pred;
|
||||||
}
|
}
|
||||||
@@ -118,6 +136,9 @@ Id EmitVoteAll(EmitContext& ctx, Id pred) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Id EmitVoteAny(EmitContext& ctx, Id pred) {
|
Id EmitVoteAny(EmitContext& ctx, Id pred) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
return pred;
|
||||||
|
}
|
||||||
if (!StageSupportsSubgroups(ctx)) {
|
if (!StageSupportsSubgroups(ctx)) {
|
||||||
return pred;
|
return pred;
|
||||||
}
|
}
|
||||||
@@ -134,6 +155,9 @@ Id EmitVoteAny(EmitContext& ctx, Id pred) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Id EmitVoteEqual(EmitContext& ctx, Id pred) {
|
Id EmitVoteEqual(EmitContext& ctx, Id pred) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
return ctx.true_value;
|
||||||
|
}
|
||||||
if (!StageSupportsSubgroups(ctx)) {
|
if (!StageSupportsSubgroups(ctx)) {
|
||||||
return ctx.true_value;
|
return ctx.true_value;
|
||||||
}
|
}
|
||||||
@@ -151,6 +175,16 @@ Id EmitVoteEqual(EmitContext& ctx, Id pred) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Id EmitSubgroupBallot(EmitContext& ctx, Id pred) {
|
Id EmitSubgroupBallot(EmitContext& ctx, Id pred) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
// Reflect only this invocation's own predicate. There is no way to observe other
|
||||||
|
// invocations' predicates without real subgroup hardware support in this stage, so this
|
||||||
|
// is a best-effort approximation: it keeps any branch gated on "did anyone match" live
|
||||||
|
// (rather than letting the SPIR-V optimizer prove it dead, which previously caused
|
||||||
|
// indirect draws fed by this shader to see indexCount=instanceCount=0), but any downstream
|
||||||
|
// math that assumes a real cross-lane population count (e.g. popcount-based compaction
|
||||||
|
// offsets) will not be correct.
|
||||||
|
return ctx.OpSelect(ctx.U32[1], pred, ctx.Const(1U), ctx.u32_zero_value);
|
||||||
|
}
|
||||||
if (!StageSupportsSubgroups(ctx)) {
|
if (!StageSupportsSubgroups(ctx)) {
|
||||||
return ctx.OpSelect(ctx.U32[1], pred, ctx.Const(1u), ctx.u32_zero_value);
|
return ctx.OpSelect(ctx.U32[1], pred, ctx.Const(1u), ctx.u32_zero_value);
|
||||||
}
|
}
|
||||||
@@ -162,6 +196,9 @@ Id EmitSubgroupBallot(EmitContext& ctx, Id pred) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Id EmitSubgroupEqMask(EmitContext& ctx) {
|
Id EmitSubgroupEqMask(EmitContext& ctx) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
return ctx.Const(1U);
|
||||||
|
}
|
||||||
if (!StageSupportsSubgroups(ctx)) {
|
if (!StageSupportsSubgroups(ctx)) {
|
||||||
return ctx.Const(1u);
|
return ctx.Const(1u);
|
||||||
}
|
}
|
||||||
@@ -169,6 +206,9 @@ Id EmitSubgroupEqMask(EmitContext& ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Id EmitSubgroupLtMask(EmitContext& ctx) {
|
Id EmitSubgroupLtMask(EmitContext& ctx) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
return ctx.u32_zero_value;
|
||||||
|
}
|
||||||
if (!StageSupportsSubgroups(ctx)) {
|
if (!StageSupportsSubgroups(ctx)) {
|
||||||
return ctx.u32_zero_value;
|
return ctx.u32_zero_value;
|
||||||
}
|
}
|
||||||
@@ -176,6 +216,9 @@ Id EmitSubgroupLtMask(EmitContext& ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Id EmitSubgroupLeMask(EmitContext& ctx) {
|
Id EmitSubgroupLeMask(EmitContext& ctx) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
return ctx.Const(1U);
|
||||||
|
}
|
||||||
if (!StageSupportsSubgroups(ctx)) {
|
if (!StageSupportsSubgroups(ctx)) {
|
||||||
return ctx.Const(1u);
|
return ctx.Const(1u);
|
||||||
}
|
}
|
||||||
@@ -183,6 +226,9 @@ Id EmitSubgroupLeMask(EmitContext& ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Id EmitSubgroupGtMask(EmitContext& ctx) {
|
Id EmitSubgroupGtMask(EmitContext& ctx) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
return ctx.u32_zero_value;
|
||||||
|
}
|
||||||
if (!StageSupportsSubgroups(ctx)) {
|
if (!StageSupportsSubgroups(ctx)) {
|
||||||
return ctx.u32_zero_value;
|
return ctx.u32_zero_value;
|
||||||
}
|
}
|
||||||
@@ -190,6 +236,9 @@ Id EmitSubgroupGtMask(EmitContext& ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Id EmitSubgroupGeMask(EmitContext& ctx) {
|
Id EmitSubgroupGeMask(EmitContext& ctx) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
return ctx.Const(1U);
|
||||||
|
}
|
||||||
if (!StageSupportsSubgroups(ctx)) {
|
if (!StageSupportsSubgroups(ctx)) {
|
||||||
return ctx.Const(1u);
|
return ctx.Const(1u);
|
||||||
}
|
}
|
||||||
@@ -198,6 +247,10 @@ Id EmitSubgroupGeMask(EmitContext& ctx) {
|
|||||||
|
|
||||||
Id EmitShuffleIndex(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp,
|
Id EmitShuffleIndex(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp,
|
||||||
Id segmentation_mask) {
|
Id segmentation_mask) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
SetInBoundsFlag(inst, ctx.false_value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
const Id not_seg_mask{ctx.OpNot(ctx.U32[1], segmentation_mask)};
|
const Id not_seg_mask{ctx.OpNot(ctx.U32[1], segmentation_mask)};
|
||||||
const Id thread_id{EmitLaneId(ctx)};
|
const Id thread_id{EmitLaneId(ctx)};
|
||||||
const Id min_thread_id{ComputeMinThreadId(ctx, thread_id, segmentation_mask)};
|
const Id min_thread_id{ComputeMinThreadId(ctx, thread_id, segmentation_mask)};
|
||||||
@@ -217,6 +270,10 @@ Id EmitShuffleIndex(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id cla
|
|||||||
|
|
||||||
Id EmitShuffleUp(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp,
|
Id EmitShuffleUp(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp,
|
||||||
Id segmentation_mask) {
|
Id segmentation_mask) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
SetInBoundsFlag(inst, ctx.false_value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
const Id thread_id{EmitLaneId(ctx)};
|
const Id thread_id{EmitLaneId(ctx)};
|
||||||
const Id max_thread_id{GetMaxThreadId(ctx, thread_id, clamp, segmentation_mask)};
|
const Id max_thread_id{GetMaxThreadId(ctx, thread_id, clamp, segmentation_mask)};
|
||||||
Id src_thread_id{ctx.OpISub(ctx.U32[1], thread_id, index)};
|
Id src_thread_id{ctx.OpISub(ctx.U32[1], thread_id, index)};
|
||||||
@@ -232,6 +289,10 @@ Id EmitShuffleUp(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp,
|
|||||||
|
|
||||||
Id EmitShuffleDown(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp,
|
Id EmitShuffleDown(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp,
|
||||||
Id segmentation_mask) {
|
Id segmentation_mask) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
SetInBoundsFlag(inst, ctx.false_value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
const Id thread_id{EmitLaneId(ctx)};
|
const Id thread_id{EmitLaneId(ctx)};
|
||||||
const Id max_thread_id{GetMaxThreadId(ctx, thread_id, clamp, segmentation_mask)};
|
const Id max_thread_id{GetMaxThreadId(ctx, thread_id, clamp, segmentation_mask)};
|
||||||
Id src_thread_id{ctx.OpIAdd(ctx.U32[1], thread_id, index)};
|
Id src_thread_id{ctx.OpIAdd(ctx.U32[1], thread_id, index)};
|
||||||
@@ -247,6 +308,10 @@ Id EmitShuffleDown(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clam
|
|||||||
|
|
||||||
Id EmitShuffleButterfly(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp,
|
Id EmitShuffleButterfly(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp,
|
||||||
Id segmentation_mask) {
|
Id segmentation_mask) {
|
||||||
|
if (NeedsGeometrySubgroupFallback(ctx)) {
|
||||||
|
SetInBoundsFlag(inst, ctx.false_value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
const Id thread_id{EmitLaneId(ctx)};
|
const Id thread_id{EmitLaneId(ctx)};
|
||||||
const Id max_thread_id{GetMaxThreadId(ctx, thread_id, clamp, segmentation_mask)};
|
const Id max_thread_id{GetMaxThreadId(ctx, thread_id, clamp, segmentation_mask)};
|
||||||
Id src_thread_id{ctx.OpBitwiseXor(ctx.U32[1], thread_id, index)};
|
Id src_thread_id{ctx.OpBitwiseXor(ctx.U32[1], thread_id, index)};
|
||||||
|
|||||||
@@ -1455,13 +1455,14 @@ void EmitContext::DefineInputs(const IR::Program& program) {
|
|||||||
if (info.uses_is_helper_invocation) {
|
if (info.uses_is_helper_invocation) {
|
||||||
is_helper_invocation = DefineInput(*this, U1, false, spv::BuiltIn::HelperInvocation);
|
is_helper_invocation = DefineInput(*this, U1, false, spv::BuiltIn::HelperInvocation);
|
||||||
}
|
}
|
||||||
if (info.uses_subgroup_mask && profile.SupportsSubgroupStage(stage)) {
|
if (info.uses_subgroup_mask &&
|
||||||
|
(stage != Stage::Geometry || profile.support_subgroup_in_geometry_stage)) {
|
||||||
subgroup_mask_eq = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupEqMaskKHR);
|
subgroup_mask_eq = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupEqMaskKHR);
|
||||||
subgroup_mask_lt = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupLtMaskKHR);
|
subgroup_mask_lt = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupLtMaskKHR);
|
||||||
subgroup_mask_le = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupLeMaskKHR);
|
subgroup_mask_le = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupLeMaskKHR);
|
||||||
subgroup_mask_gt = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupGtMaskKHR);
|
subgroup_mask_gt = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupGtMaskKHR);
|
||||||
subgroup_mask_ge = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupGeMaskKHR);
|
subgroup_mask_ge = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupGeMaskKHR);
|
||||||
if (stage == Stage::Fragment) {
|
if (profile.support_explicit_workgroup_layout) {
|
||||||
Decorate(subgroup_mask_eq, spv::Decoration::Flat);
|
Decorate(subgroup_mask_eq, spv::Decoration::Flat);
|
||||||
Decorate(subgroup_mask_lt, spv::Decoration::Flat);
|
Decorate(subgroup_mask_lt, spv::Decoration::Flat);
|
||||||
Decorate(subgroup_mask_le, spv::Decoration::Flat);
|
Decorate(subgroup_mask_le, spv::Decoration::Flat);
|
||||||
@@ -1469,10 +1470,11 @@ void EmitContext::DefineInputs(const IR::Program& program) {
|
|||||||
Decorate(subgroup_mask_ge, spv::Decoration::Flat);
|
Decorate(subgroup_mask_ge, spv::Decoration::Flat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((info.uses_fswzadd || info.uses_subgroup_invocation_id || info.uses_subgroup_shuffles ||
|
if (info.uses_fswzadd ||
|
||||||
|
((info.uses_subgroup_invocation_id || info.uses_subgroup_shuffles ||
|
||||||
(profile.warp_size_potentially_larger_than_guest &&
|
(profile.warp_size_potentially_larger_than_guest &&
|
||||||
(info.uses_subgroup_vote || info.uses_subgroup_mask))) &&
|
(info.uses_subgroup_vote || info.uses_subgroup_mask))) &&
|
||||||
profile.SupportsSubgroupStage(stage)) {
|
(stage != Stage::Geometry || profile.support_subgroup_in_geometry_stage))) {
|
||||||
AddCapability(spv::Capability::GroupNonUniform);
|
AddCapability(spv::Capability::GroupNonUniform);
|
||||||
subgroup_local_invocation_id =
|
subgroup_local_invocation_id =
|
||||||
DefineInput(*this, U32[1], false, spv::BuiltIn::SubgroupLocalInvocationId);
|
DefineInput(*this, U32[1], false, spv::BuiltIn::SubgroupLocalInvocationId);
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ struct Profile {
|
|||||||
bool support_quad_shuffles{};
|
bool support_quad_shuffles{};
|
||||||
bool support_vote{};
|
bool support_vote{};
|
||||||
u32 supported_subgroup_stages{0x7F};
|
u32 supported_subgroup_stages{0x7F};
|
||||||
|
bool support_subgroup_in_geometry_stage{}; ///< True when the device advertises subgroup
|
||||||
|
///< ballot/shuffle support for VK_SHADER_STAGE_GEOMETRY_BIT
|
||||||
|
///< (VkPhysicalDeviceSubgroupProperties::supportedStages).
|
||||||
|
///< Many mobile GPUs support subgroup ops only in
|
||||||
|
///< fragment/compute; guest shaders using VOTE/SHFL in a
|
||||||
|
///< geometry program need a non-subgroup fallback there.
|
||||||
bool support_viewport_index_layer_non_geometry{};
|
bool support_viewport_index_layer_non_geometry{};
|
||||||
bool support_viewport_mask{};
|
bool support_viewport_mask{};
|
||||||
bool support_typeless_image_loads{};
|
bool support_typeless_image_loads{};
|
||||||
|
|||||||
@@ -1281,7 +1281,7 @@ VkPipeline BlitImageHelper::FindOrEmplaceColorPipeline(const BlitImagePipelineKe
|
|||||||
.subpass = 0,
|
.subpass = 0,
|
||||||
.basePipelineHandle = VK_NULL_HANDLE,
|
.basePipelineHandle = VK_NULL_HANDLE,
|
||||||
.basePipelineIndex = 0,
|
.basePipelineIndex = 0,
|
||||||
}));
|
}, device.StaticPipelineCache()));
|
||||||
return *blit_color_pipelines.back();
|
return *blit_color_pipelines.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1313,7 +1313,7 @@ VkPipeline BlitImageHelper::FindOrEmplaceDepthStencilPipeline(const BlitImagePip
|
|||||||
.subpass = 0,
|
.subpass = 0,
|
||||||
.basePipelineHandle = VK_NULL_HANDLE,
|
.basePipelineHandle = VK_NULL_HANDLE,
|
||||||
.basePipelineIndex = 0,
|
.basePipelineIndex = 0,
|
||||||
}));
|
}, device.StaticPipelineCache()));
|
||||||
return *blit_depth_stencil_pipelines.back();
|
return *blit_depth_stencil_pipelines.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1366,7 +1366,7 @@ VkPipeline BlitImageHelper::FindOrEmplaceClearColorPipeline(const BlitImagePipel
|
|||||||
.subpass = 0,
|
.subpass = 0,
|
||||||
.basePipelineHandle = VK_NULL_HANDLE,
|
.basePipelineHandle = VK_NULL_HANDLE,
|
||||||
.basePipelineIndex = 0,
|
.basePipelineIndex = 0,
|
||||||
}));
|
}, device.StaticPipelineCache()));
|
||||||
return *clear_color_pipelines.back();
|
return *clear_color_pipelines.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1422,7 +1422,7 @@ VkPipeline BlitImageHelper::FindOrEmplaceClearStencilPipeline(
|
|||||||
.subpass = 0,
|
.subpass = 0,
|
||||||
.basePipelineHandle = VK_NULL_HANDLE,
|
.basePipelineHandle = VK_NULL_HANDLE,
|
||||||
.basePipelineIndex = 0,
|
.basePipelineIndex = 0,
|
||||||
}));
|
}, device.StaticPipelineCache()));
|
||||||
return *clear_stencil_pipelines.back();
|
return *clear_stencil_pipelines.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1465,7 +1465,7 @@ VkPipeline BlitImageHelper::FindOrEmplaceBlitColorMSAAPipeline(const BlitMSAAPip
|
|||||||
.subpass = 0,
|
.subpass = 0,
|
||||||
.basePipelineHandle = VK_NULL_HANDLE,
|
.basePipelineHandle = VK_NULL_HANDLE,
|
||||||
.basePipelineIndex = 0,
|
.basePipelineIndex = 0,
|
||||||
}));
|
}, device.StaticPipelineCache()));
|
||||||
return *blit_msaa_color_pipelines.back();
|
return *blit_msaa_color_pipelines.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1584,7 +1584,7 @@ VkPipeline BlitImageHelper::FindOrEmplaceResolveDepthStencilPipeline(VkRenderPas
|
|||||||
.subpass = 0,
|
.subpass = 0,
|
||||||
.basePipelineHandle = VK_NULL_HANDLE,
|
.basePipelineHandle = VK_NULL_HANDLE,
|
||||||
.basePipelineIndex = 0,
|
.basePipelineIndex = 0,
|
||||||
}));
|
}, device.StaticPipelineCache()));
|
||||||
return *pipelines.back();
|
return *pipelines.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1697,7 +1697,7 @@ VkPipeline BlitImageHelper::FindOrEmplaceMSAACopyPipeline(const MSAACopyPipeline
|
|||||||
.subpass = 0,
|
.subpass = 0,
|
||||||
.basePipelineHandle = VK_NULL_HANDLE,
|
.basePipelineHandle = VK_NULL_HANDLE,
|
||||||
.basePipelineIndex = 0,
|
.basePipelineIndex = 0,
|
||||||
}));
|
}, device.StaticPipelineCache()));
|
||||||
return *msaa_copy_pipelines.back();
|
return *msaa_copy_pipelines.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1817,7 +1817,7 @@ void BlitImageHelper::ConvertPipelineEx(vk::Pipeline& pipeline, VkRenderPass ren
|
|||||||
.subpass = 0,
|
.subpass = 0,
|
||||||
.basePipelineHandle = VK_NULL_HANDLE,
|
.basePipelineHandle = VK_NULL_HANDLE,
|
||||||
.basePipelineIndex = 0,
|
.basePipelineIndex = 0,
|
||||||
});
|
}, device.StaticPipelineCache());
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlitImageHelper::ConvertPipelineColorTargetEx(vk::Pipeline& pipeline, VkRenderPass renderpass,
|
void BlitImageHelper::ConvertPipelineColorTargetEx(vk::Pipeline& pipeline, VkRenderPass renderpass,
|
||||||
@@ -1860,7 +1860,7 @@ void BlitImageHelper::ConvertPipeline(vk::Pipeline& pipeline, VkRenderPass rende
|
|||||||
.subpass = 0,
|
.subpass = 0,
|
||||||
.basePipelineHandle = VK_NULL_HANDLE,
|
.basePipelineHandle = VK_NULL_HANDLE,
|
||||||
.basePipelineIndex = 0,
|
.basePipelineIndex = 0,
|
||||||
});
|
}, device.StaticPipelineCache());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Vulkan
|
} // namespace Vulkan
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ static vk::Pipeline CreateWrappedPipelineImpl(
|
|||||||
.subpass = 0,
|
.subpass = 0,
|
||||||
.basePipelineHandle = 0,
|
.basePipelineHandle = 0,
|
||||||
.basePipelineIndex = 0,
|
.basePipelineIndex = 0,
|
||||||
});
|
}, device.StaticPipelineCache());
|
||||||
}
|
}
|
||||||
|
|
||||||
vk::Pipeline CreateWrappedPipeline(const Device& device, vk::RenderPass& renderpass,
|
vk::Pipeline CreateWrappedPipeline(const Device& device, vk::RenderPass& renderpass,
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ ComputePass::ComputePass(const Device& device_, Scheduler& scheduler, Descriptor
|
|||||||
.layout = *layout,
|
.layout = *layout,
|
||||||
.basePipelineHandle = {},
|
.basePipelineHandle = {},
|
||||||
.basePipelineIndex = 0,
|
.basePipelineIndex = 0,
|
||||||
});
|
}, device.StaticPipelineCache());
|
||||||
}
|
}
|
||||||
|
|
||||||
ComputePass::~ComputePass() = default;
|
ComputePass::~ComputePass() = default;
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ using VideoCommon::GenericEnvironment;
|
|||||||
using VideoCommon::GraphicsEnvironment;
|
using VideoCommon::GraphicsEnvironment;
|
||||||
|
|
||||||
constexpr u32 CACHE_VERSION = 18;
|
constexpr u32 CACHE_VERSION = 18;
|
||||||
|
constexpr size_t VULKAN_CACHE_FLUSH_PIPELINES = 128;
|
||||||
|
constexpr size_t VULKAN_CACHE_FLUSH_MIN_SECONDS = 30;
|
||||||
constexpr std::array<char, 8> VULKAN_CACHE_MAGIC_NUMBER{'y', 'u', 'z', 'u', 'v', 'k', 'c', 'h'};
|
constexpr std::array<char, 8> VULKAN_CACHE_MAGIC_NUMBER{'y', 'u', 'z', 'u', 'v', 'k', 'c', 'h'};
|
||||||
|
|
||||||
template <typename Container>
|
template <typename Container>
|
||||||
@@ -408,6 +410,11 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
|
|||||||
.support_quad_shuffles = device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_QUAD_BIT),
|
.support_quad_shuffles = device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_QUAD_BIT),
|
||||||
.support_vote = device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_VOTE_BIT),
|
.support_vote = device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_VOTE_BIT),
|
||||||
.supported_subgroup_stages = supported_subgroup_stages,
|
.supported_subgroup_stages = supported_subgroup_stages,
|
||||||
|
.support_subgroup_in_geometry_stage =
|
||||||
|
device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_VOTE_BIT) &&
|
||||||
|
device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_BALLOT_BIT) &&
|
||||||
|
device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_SHUFFLE_BIT) &&
|
||||||
|
device.IsSubgroupFeatureSupportedInStage(VK_SHADER_STAGE_GEOMETRY_BIT),
|
||||||
.support_viewport_index_layer_non_geometry =
|
.support_viewport_index_layer_non_geometry =
|
||||||
device.IsExtShaderViewportIndexLayerSupported(),
|
device.IsExtShaderViewportIndexLayerSupported(),
|
||||||
.support_viewport_mask = device.IsNvViewportArray2Supported(),
|
.support_viewport_mask = device.IsNvViewportArray2Supported(),
|
||||||
@@ -710,6 +717,10 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
|
|||||||
if (use_vulkan_pipeline_cache) {
|
if (use_vulkan_pipeline_cache) {
|
||||||
SerializeVulkanPipelineCache(vulkan_pipeline_cache_filename, vulkan_pipeline_cache,
|
SerializeVulkanPipelineCache(vulkan_pipeline_cache_filename, vulkan_pipeline_cache,
|
||||||
CACHE_VERSION);
|
CACHE_VERSION);
|
||||||
|
size_t size = 0;
|
||||||
|
vulkan_pipeline_cache.Read(&size, nullptr);
|
||||||
|
last_cache_size.store(size, std::memory_order_relaxed);
|
||||||
|
last_flush = std::chrono::steady_clock::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.statistics) {
|
if (state.statistics) {
|
||||||
@@ -717,6 +728,35 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PipelineCache::QueueVulkanPipelineCacheFlush() {
|
||||||
|
if (!use_vulkan_pipeline_cache || vulkan_pipeline_cache_filename.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (++pipelines_since_flush < VULKAN_CACHE_FLUSH_PIPELINES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
const auto megabytes = last_cache_size.load(std::memory_order_relaxed) / (1024 * 1024);
|
||||||
|
const std::chrono::seconds interval{
|
||||||
|
std::max<size_t>(VULKAN_CACHE_FLUSH_MIN_SECONDS, megabytes)};
|
||||||
|
if (last_flush.time_since_epoch().count() != 0 && now - last_flush < interval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (flush_in_flight.exchange(true, std::memory_order_acq_rel)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pipelines_since_flush = 0;
|
||||||
|
last_flush = now;
|
||||||
|
serialization_thread.QueueWork([this] {
|
||||||
|
SerializeVulkanPipelineCache(vulkan_pipeline_cache_filename, vulkan_pipeline_cache,
|
||||||
|
CACHE_VERSION);
|
||||||
|
size_t size = 0;
|
||||||
|
vulkan_pipeline_cache.Read(&size, nullptr);
|
||||||
|
last_cache_size.store(size, std::memory_order_relaxed);
|
||||||
|
flush_in_flight.store(false, std::memory_order_release);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
GraphicsPipeline* PipelineCache::CurrentGraphicsPipelineSlowPath() {
|
GraphicsPipeline* PipelineCache::CurrentGraphicsPipelineSlowPath() {
|
||||||
const auto [pair, is_new]{graphics_cache.try_emplace(graphics_key)};
|
const auto [pair, is_new]{graphics_cache.try_emplace(graphics_key)};
|
||||||
auto& pipeline{pair->second};
|
auto& pipeline{pair->second};
|
||||||
@@ -755,7 +795,7 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
|
|||||||
std::span<Shader::Environment* const> envs, PipelineStatistics* statistics,
|
std::span<Shader::Environment* const> envs, PipelineStatistics* statistics,
|
||||||
bool build_in_parallel) try {
|
bool build_in_parallel) try {
|
||||||
auto hash = key.Hash();
|
auto hash = key.Hash();
|
||||||
LOG_INFO(Render_Vulkan, "{:#016x}", hash);
|
LOG_DEBUG(Render_Vulkan, "{:#016x}", hash);
|
||||||
size_t env_index{0};
|
size_t env_index{0};
|
||||||
std::array<Shader::IR::Program, Maxwell::MaxShaderProgram> programs;
|
std::array<Shader::IR::Program, Maxwell::MaxShaderProgram> programs;
|
||||||
const bool uses_vertex_a{key.unique_hashes[0] != 0};
|
const bool uses_vertex_a{key.unique_hashes[0] != 0};
|
||||||
@@ -891,6 +931,7 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline() {
|
|||||||
}
|
}
|
||||||
SerializePipeline(key, env_ptrs, pipeline_cache_filename, CACHE_VERSION);
|
SerializePipeline(key, env_ptrs, pipeline_cache_filename, CACHE_VERSION);
|
||||||
});
|
});
|
||||||
|
QueueVulkanPipelineCacheFlush();
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -910,6 +951,7 @@ std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
|
|||||||
SerializePipeline(key, std::array<const GenericEnvironment*, 1>{&env_},
|
SerializePipeline(key, std::array<const GenericEnvironment*, 1>{&env_},
|
||||||
pipeline_cache_filename, CACHE_VERSION);
|
pipeline_cache_filename, CACHE_VERSION);
|
||||||
});
|
});
|
||||||
|
QueueVulkanPipelineCacheFlush();
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -922,7 +964,7 @@ std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO(Render_Vulkan, "{:#016x}", hash);
|
LOG_DEBUG(Render_Vulkan, "{:#016x}", hash);
|
||||||
|
|
||||||
Shader::Maxwell::Flow::CFG cfg{env, pools.flow_block, env.StartAddress()};
|
Shader::Maxwell::Flow::CFG cfg{env, pools.flow_block, env.StartAddress()};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -144,6 +146,8 @@ private:
|
|||||||
vk::PipelineCache LoadVulkanPipelineCache(const std::filesystem::path& filename,
|
vk::PipelineCache LoadVulkanPipelineCache(const std::filesystem::path& filename,
|
||||||
u32 expected_cache_version);
|
u32 expected_cache_version);
|
||||||
|
|
||||||
|
void QueueVulkanPipelineCacheFlush();
|
||||||
|
|
||||||
const Device& device;
|
const Device& device;
|
||||||
Scheduler& scheduler;
|
Scheduler& scheduler;
|
||||||
DescriptorPool& descriptor_pool;
|
DescriptorPool& descriptor_pool;
|
||||||
@@ -171,6 +175,10 @@ private:
|
|||||||
|
|
||||||
std::filesystem::path vulkan_pipeline_cache_filename;
|
std::filesystem::path vulkan_pipeline_cache_filename;
|
||||||
vk::PipelineCache vulkan_pipeline_cache;
|
vk::PipelineCache vulkan_pipeline_cache;
|
||||||
|
size_t pipelines_since_flush{};
|
||||||
|
std::chrono::steady_clock::time_point last_flush{};
|
||||||
|
std::atomic<size_t> last_cache_size{};
|
||||||
|
std::atomic_bool flush_in_flight{};
|
||||||
|
|
||||||
Common::ThreadWorker workers;
|
Common::ThreadWorker workers;
|
||||||
Common::ThreadWorker serialization_thread;
|
Common::ThreadWorker serialization_thread;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <bitset>
|
#include <bitset>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <ankerl/unordered_dense.h>
|
#include <ankerl/unordered_dense.h>
|
||||||
@@ -16,6 +18,8 @@
|
|||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
|
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
|
#include "common/fs/fs.h"
|
||||||
|
#include "common/fs/path_util.h"
|
||||||
#include "common/literals.h"
|
#include "common/literals.h"
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
@@ -393,6 +397,17 @@ std::vector<const char*> ExtensionListForVulkan(
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr std::array<char, 8> STATIC_CACHE_MAGIC_NUMBER{'e', 'd', 'e', 'n', 's', 't', 'p', 'c'};
|
||||||
|
constexpr u32 STATIC_CACHE_VERSION = 1;
|
||||||
|
|
||||||
|
std::filesystem::path StaticPipelineCacheFilename() {
|
||||||
|
const auto shader_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::ShaderDir);
|
||||||
|
if (!Common::FS::CreateDir(shader_dir)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return shader_dir / "vulkan_static_pipelines.bin";
|
||||||
|
}
|
||||||
|
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
||||||
void Device::RemoveExtension(bool& extension, const std::string& extension_name) {
|
void Device::RemoveExtension(bool& extension, const std::string& extension_name) {
|
||||||
@@ -750,15 +765,100 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
|
|||||||
|
|
||||||
vk::Check(vmaCreateAllocator(&allocator_info, &allocator));
|
vk::Check(vmaCreateAllocator(&allocator_info, &allocator));
|
||||||
|
|
||||||
|
owns_static_pipeline_cache = surface != VkSurfaceKHR{};
|
||||||
|
LoadStaticPipelineCache();
|
||||||
|
|
||||||
// Initialize GPU logging if enabled
|
// Initialize GPU logging if enabled
|
||||||
InitializeGPULogging();
|
InitializeGPULogging();
|
||||||
}
|
}
|
||||||
|
|
||||||
Device::~Device() {
|
Device::~Device() {
|
||||||
|
SaveStaticPipelineCache();
|
||||||
ShutdownGPULogging();
|
ShutdownGPULogging();
|
||||||
vmaDestroyAllocator(allocator);
|
vmaDestroyAllocator(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Device::LoadStaticPipelineCache() {
|
||||||
|
const auto create = [this](size_t size, const void* data) {
|
||||||
|
static_pipeline_cache = logical.CreatePipelineCache({
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.initialDataSize = size,
|
||||||
|
.pInitialData = data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (!owns_static_pipeline_cache) {
|
||||||
|
create(0, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto filename = StaticPipelineCacheFilename();
|
||||||
|
if (filename.empty()) {
|
||||||
|
create(0, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::vector<char> data;
|
||||||
|
try {
|
||||||
|
std::ifstream file(filename, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
create(0, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
|
||||||
|
const size_t total = static_cast<size_t>(file.tellg());
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
std::array<char, 8> magic{};
|
||||||
|
u32 version{};
|
||||||
|
if (total < magic.size() + sizeof(version)) {
|
||||||
|
create(0, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file.read(magic.data(), magic.size())
|
||||||
|
.read(reinterpret_cast<char*>(&version), sizeof(version));
|
||||||
|
if (magic != STATIC_CACHE_MAGIC_NUMBER || version != STATIC_CACHE_VERSION) {
|
||||||
|
create(0, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.resize(total - magic.size() - sizeof(version));
|
||||||
|
file.read(data.data(), static_cast<std::streamsize>(data.size()));
|
||||||
|
} catch (const std::ios_base::failure& e) {
|
||||||
|
create(0, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
create(data.size(), data.empty() ? nullptr : data.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Device::SaveStaticPipelineCache() const {
|
||||||
|
if (!owns_static_pipeline_cache || !static_pipeline_cache) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto filename = StaticPipelineCacheFilename();
|
||||||
|
if (filename.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t size = 0;
|
||||||
|
std::vector<char> data;
|
||||||
|
static_pipeline_cache.Read(&size, nullptr);
|
||||||
|
if (size == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.resize(size);
|
||||||
|
static_pipeline_cache.Read(&size, data.data());
|
||||||
|
try {
|
||||||
|
std::ofstream file(filename, std::ios::binary | std::ios::trunc);
|
||||||
|
file.exceptions(std::ofstream::failbit);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file.write(STATIC_CACHE_MAGIC_NUMBER.data(), STATIC_CACHE_MAGIC_NUMBER.size())
|
||||||
|
.write(reinterpret_cast<const char*>(&STATIC_CACHE_VERSION),
|
||||||
|
sizeof(STATIC_CACHE_VERSION))
|
||||||
|
.write(data.data(), static_cast<std::streamsize>(size));
|
||||||
|
} catch (const std::ios_base::failure& e) {
|
||||||
|
Common::FS::RemoveFile(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VkFormat Device::GetSupportedFormat(VkFormat wanted_format, VkFormatFeatureFlags wanted_usage,
|
VkFormat Device::GetSupportedFormat(VkFormat wanted_format, VkFormatFeatureFlags wanted_usage,
|
||||||
FormatType format_type) const {
|
FormatType format_type) const {
|
||||||
if (IsFormatSupported(wanted_format, wanted_usage, format_type)) {
|
if (IsFormatSupported(wanted_format, wanted_usage, format_type)) {
|
||||||
|
|||||||
@@ -273,6 +273,10 @@ public:
|
|||||||
return physical;
|
return physical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VkPipelineCache StaticPipelineCache() const noexcept {
|
||||||
|
return *static_pipeline_cache;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the main graphics queue.
|
/// Returns the main graphics queue.
|
||||||
vk::Queue GetGraphicsQueue() const {
|
vk::Queue GetGraphicsQueue() const {
|
||||||
return graphics_queue;
|
return graphics_queue;
|
||||||
@@ -475,6 +479,11 @@ FN_MAX_LIMIT_LIST
|
|||||||
return properties.subgroup_properties.supportedStages;
|
return properties.subgroup_properties.supportedStages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the device supports subgroup ballot/shuffle in the given shader stage.
|
||||||
|
bool IsSubgroupFeatureSupportedInStage(VkShaderStageFlagBits stage) const {
|
||||||
|
return properties.subgroup_properties.supportedStages & stage;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the maximum number of push descriptors.
|
/// Returns the maximum number of push descriptors.
|
||||||
u32 MaxPushDescriptors() const {
|
u32 MaxPushDescriptors() const {
|
||||||
return properties.push_descriptor.maxPushDescriptors;
|
return properties.push_descriptor.maxPushDescriptors;
|
||||||
@@ -1127,6 +1136,9 @@ private:
|
|||||||
/// Returns true if the device natively supports blitting depth stencil images.
|
/// Returns true if the device natively supports blitting depth stencil images.
|
||||||
bool TestDepthStencilBlits(VkFormat format) const;
|
bool TestDepthStencilBlits(VkFormat format) const;
|
||||||
|
|
||||||
|
void LoadStaticPipelineCache();
|
||||||
|
void SaveStaticPipelineCache() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
VkInstance instance; ///< Vulkan instance.
|
VkInstance instance; ///< Vulkan instance.
|
||||||
VmaAllocator allocator; ///< VMA allocator.
|
VmaAllocator allocator; ///< VMA allocator.
|
||||||
@@ -1135,6 +1147,8 @@ private:
|
|||||||
vk::Device logical; ///< Logical device.
|
vk::Device logical; ///< Logical device.
|
||||||
vk::Queue graphics_queue; ///< Main graphics queue.
|
vk::Queue graphics_queue; ///< Main graphics queue.
|
||||||
vk::Queue present_queue; ///< Main present queue.
|
vk::Queue present_queue; ///< Main present queue.
|
||||||
|
vk::PipelineCache static_pipeline_cache;
|
||||||
|
bool owns_static_pipeline_cache{};
|
||||||
u32 instance_version{}; ///< Vulkan instance version.
|
u32 instance_version{}; ///< Vulkan instance version.
|
||||||
u32 graphics_family{}; ///< Main graphics queue family index.
|
u32 graphics_family{}; ///< Main graphics queue family index.
|
||||||
u32 present_family{}; ///< Main present queue family index.
|
u32 present_family{}; ///< Main present queue family index.
|
||||||
|
|||||||
Reference in New Issue
Block a user