mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-09-15 15:58:23 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7510b8e3d2 | |||
| 9cc52e613d | |||
| 4242847637 | |||
| 5adaa5b0f7 | |||
| 2b4184dd2b | |||
| d9159fefdd | |||
| 099547b3f4 | |||
| 93318ef697 | |||
| 9b64944480 | |||
| 5d150cac5c | |||
| ff8368c606 |
@@ -53,6 +53,7 @@ if (YUZU_STATIC_ROOM)
|
||||
# disable e v e r y t h i n g
|
||||
set(ENABLE_QT OFF)
|
||||
set(YUZU_CMD OFF)
|
||||
set(ENABLE_CUBEB OFF)
|
||||
set(ENABLE_UPDATE_CHECKER OFF)
|
||||
set(USE_DISCORD_PRESENCE OFF)
|
||||
set(BUILD_TESTING OFF)
|
||||
@@ -216,6 +217,8 @@ if(MSVC)
|
||||
endif()
|
||||
|
||||
# TODO(crueter): Cleanup, each dep that has a bundled option should allow to choose between bundled, external, system
|
||||
option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
|
||||
|
||||
set(EXT_DEFAULT OFF)
|
||||
if (MSVC OR ANDROID)
|
||||
set(EXT_DEFAULT ON)
|
||||
@@ -574,6 +577,10 @@ if (NOT YUZU_STATIC_ROOM)
|
||||
find_package(DiscordRPC)
|
||||
endif()
|
||||
|
||||
if (ENABLE_CUBEB)
|
||||
find_package(cubeb)
|
||||
endif()
|
||||
|
||||
if (YUZU_TESTS OR DYNARMIC_TESTS)
|
||||
find_package(Catch2)
|
||||
endif()
|
||||
|
||||
@@ -46,6 +46,18 @@
|
||||
"repo": "arun11299/cpp-jwt",
|
||||
"version": "7f24eb4c32"
|
||||
},
|
||||
"cubeb": {
|
||||
"find_args": "CONFIG",
|
||||
"hash": "8a4bcb2f83ba590f52c66626e895304a73eb61928dbc57777e1822e55378e3568366f17f9da4b80036cc2ef4ea9723c32abf6e7d9bbe00fb03654f0991596ab0",
|
||||
"options": [
|
||||
"USE_SANITIZERS OFF",
|
||||
"BUILD_TESTS OFF",
|
||||
"BUILD_TOOLS OFF",
|
||||
"BUNDLE_SPEEX ON"
|
||||
],
|
||||
"repo": "mozilla/cubeb",
|
||||
"version": "fa02160712"
|
||||
},
|
||||
"discord-rpc": {
|
||||
"find_args": "MODULE",
|
||||
"hash": "8d680b3a16d6f6bf292ad823cf8635595ff986f2a49f758e3009f505b540a9d75194a8cf44cddea75736a8c3e3b5160166e716a0939ce3f18cc463cf648753fe",
|
||||
|
||||
@@ -100,6 +100,8 @@ For reasons unberknownst to any human being, `glslangValidator` will crash upon
|
||||
|
||||
For this reason this patch is NOT applied to default on all platforms (for obvious reasons) - instead this is a HaikuOS specific patch, apply with `git apply <absolute path to patch>` after cloning SPIRV-Tools then `make -C build` and add the resulting binary (in `build/StandAlone/glslang`) into PATH.
|
||||
|
||||
`cubeb_devel` will also not work, either disable cubeb or uninstall it.
|
||||
|
||||
Still will not run flawlessly until `mesa-24` is available. Modify CMakeCache.txt with the `.so` of libGL and libGLESv2 by doing the incredibly difficult task of copy pasting them (`cp /boot/system/lib/libGL.so .`)
|
||||
|
||||
If you have `quazip1_qt6_devel`, uninstall it. It may call `Core5Compat` on CMake which is wrongly packaged.
|
||||
|
||||
+3
-2
@@ -73,6 +73,7 @@ Certain other dependencies will be fetched by CPM regardless. System packages *c
|
||||
|
||||
* [SimpleIni](https://github.com/brofield/simpleini)
|
||||
* [DiscordRPC](https://github.com/eden-emulator/discord-rpc)
|
||||
* [cubeb](https://github.com/mozilla/cubeb)
|
||||
* [libusb](https://github.com/libusb/libusb)
|
||||
* [VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator)
|
||||
* [sirit](https://github.com/eden-emulator/sirit)
|
||||
@@ -121,7 +122,7 @@ sudo emerge -a \
|
||||
dev-util/spirv-tools dev-util/spirv-headers dev-util/vulkan-headers \
|
||||
dev-util/vulkan-utility-libraries dev-util/glslang \
|
||||
media-gfx/renderdoc media-libs/libva media-libs/opus media-video/ffmpeg \
|
||||
media-libs/VulkanMemoryAllocator media-libs/libsdl3 \
|
||||
media-libs/VulkanMemoryAllocator media-libs/libsdl3 media-libs/cubeb \
|
||||
net-libs/enet \
|
||||
sys-libs/zlib \
|
||||
dev-cpp/nlohmann_json dev-cpp/simpleini dev-cpp/cpp-httplib dev-cpp/cpp-jwt \
|
||||
@@ -355,7 +356,7 @@ pacman -Syuu --needed --noconfirm $packages
|
||||
<summary>HaikuOS</summary>
|
||||
|
||||
```sh
|
||||
pkgman install git cmake patch libfmt_devel nlohmann_json lz4_devel opus_devel boost1.90_devel vulkan_devel qt6_base_devel qt6_declarative_devel libsdl3_devel ffmpeg7_devel libx11_devel enet_devel catch2_devel quazip1_qt5_devel qt6_5compat_devel glslang qt6_devel qt6_charts_devel simpleini quazip_qt6_devel
|
||||
pkgman install git cmake patch libfmt_devel nlohmann_json lz4_devel opus_devel boost1.90_devel vulkan_devel qt6_base_devel qt6_declarative_devel libsdl3_devel ffmpeg7_devel libx11_devel enet_devel catch2_devel quazip1_qt5_devel qt6_5compat_devel glslang qt6_devel qt6_charts_devel cubeb_devel simpleini quazip_qt6_devel
|
||||
```
|
||||
|
||||
[Caveats](./Caveats.md#haikuos).
|
||||
|
||||
@@ -4,7 +4,7 @@ Modern game consoles require heavy power to be emulated appropriately. This is w
|
||||
|
||||
For example, take a disk write, instead of emulating a proper SD card we instead use the C++ standard library for I/O. Additionally we use the abstractions provided by the `fs` service to "lie" to programs about certain SD card properties. Notably this includes making up sizes for the fake SD card, giving "realistic" values or expected outputs for a given card, and so on. And instead of writing to an actual SD card, the emulator simply writes to a file.
|
||||
|
||||
This also means grand part of the emulator consists of just re-implementing firmware but using HLE primitives; for example audio doesn't go to an emulated audio device, but rather gets processed on the fly by a dedicated service and then passed to SDL3 or null backend.
|
||||
This also means grand part of the emulator consists of just re-implementing firmware but using HLE primitives; for example audio doesn't go to an emulated audio device, but rather gets processed on the fly by a dedicated service and then passed to SDL3/cubeb/etc.
|
||||
|
||||
As such, many of the systems implemented are not 100% accurate to the original software, but they're "good enough" to pass as being so. While we do strive to maintain high compatibility (especially with homebrew), there are realistic limitations to these approaches.
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ These options control dependencies.
|
||||
|
||||
- `ENABLE_WEB_SERVICE` (ON) Enable multiplayer service
|
||||
- `ENABLE_WIFI_SCAN` (OFF) Enable WiFi scanning (requires iw on Linux) - experimental
|
||||
- `ENABLE_CUBEB` (ON) Enables the cubeb audio backend
|
||||
- This option is subject for removal.
|
||||
- `YUZU_TESTS` (ON) Compile tests - requires Catch2
|
||||
- `ENABLE_LTO` (OFF) Enable link-time optimization
|
||||
- Not recommended on Windows
|
||||
|
||||
+33
-21
@@ -1,32 +1,44 @@
|
||||
# User Handbook - Command Line
|
||||
|
||||
There are two main applications, an SDL-based app (`eden-cli`) and a Qt based app (`eden`); both accept command line arguments.
|
||||
|
||||
## eden
|
||||
There are two main applications, an SDL-based app (`eden-cli`) and a Qt based app (`eden`); both accept the same command line arguments.
|
||||
|
||||
- `./eden <path>`: Running with a single argument and nothing else, will make the emulator look for the given file and load it, this behavior is similar to `eden-cli`; allows dragging and dropping games into the application.
|
||||
- `-g <path>`: Alternate way to specify what to load, overrides. However let it be noted that arguments that use `-` will be treated as options/ignored, if your game, for some reason, starts with `-`, in order to safely handle it you may need to specify it as an argument.
|
||||
- `-f`: Use fullscreen.
|
||||
- `-u <number>`: Select the index of the user to load as.
|
||||
- `-input-profile <name>`: Specifies input profile name to use (for player #0 only).
|
||||
- `--debug/-d`: Enter debug mode, allow gdb stub at port `1234`
|
||||
- `--config/-c`: Specify alternate configuration file.
|
||||
- `--fullscreen/-f`: Use fullscreen.
|
||||
- `--help/-h`: Display help.
|
||||
- `--game/-g <path>`: Alternate way to specify what to load, overrides. However let it be noted that arguments that use `-` will be treated as options/ignored, if your game, for some reason, starts with `-`, in order to safely handle it you may need to specify it as an argument.
|
||||
- `--multiplayer/-m`: Specify multiplayer options.
|
||||
- `--program/-p`: Specify the program arguments to pass (optional).
|
||||
- `--user/-u <number>`: Specify the user index.
|
||||
- `--version/-v`: Display version and quit.
|
||||
- `--input-profile/-i <name>`: Specifies input profile name to use (for player #0 only).
|
||||
- `--null-render/-n`: Forces the usage of the "Null" render backend irrespective of settings.
|
||||
- `--filter/-x`: Sets the debug log filter irrespective of settings.
|
||||
- `--singlecore/-s`: Forces single-core regardless of settings.
|
||||
|
||||
Only the Qt frontend supports the following arguments:
|
||||
|
||||
- `-qlaunch`: Launch QLaunch.
|
||||
- `-hlaunch`: Launch homebrew launcher `nx-hbloader`.
|
||||
- Requires a copy of Atmosphere to be extracted onto `sdmc`.
|
||||
- This is a shorthand for `<eden folder>/sdmc/atmosphere/hbl.nsp`.
|
||||
- `-setup`: Launch setup applet.
|
||||
|
||||
## eden-cli
|
||||
`eden` (provided with `--room`), and `eden-room` supports the following options as well:
|
||||
|
||||
- `--debug/-d`: Enter debug mode, allow gdb stub at port `1234`
|
||||
- `--config/-c`: Specify alternate configuration file.
|
||||
- `--fullscreen/-f`: Set fullscreen.
|
||||
- `--help/-h`: Display help.
|
||||
- `--game/-g`: Specify the game to run.
|
||||
- `--multiplayer/-m`: Specify multiplayer options.
|
||||
- `--program/-p`: Specify the program arguments to pass (optional).
|
||||
- `--user/-u`: Specify the user index.
|
||||
- `--version/-v`: Display version and quit.
|
||||
- `--input-profile/-i`: Specifies input profile name to use (for player #0 only).
|
||||
- `--null-render/-n`: Forces the usage of the "Null" render backend irrespective of settings.
|
||||
- `--filter/-x`: Sets the debug log filter irrespective of settings.
|
||||
- `--singlecore/-s`: Forces single-core regardless of settings.
|
||||
- `-n/--room-name`: The name of the room.
|
||||
- `-d/--room-description`: The room description.
|
||||
- `-s/--bind-address`: The bind address for the room.
|
||||
- `-p/--port`: The port used for the room.
|
||||
- `-m/--max-members`: The maximum number of players for this room.
|
||||
- `-w/--password`: The password for the room.
|
||||
- `-g/--preferred-game`: The preferred game for this room.
|
||||
- `-i/--preferred-game-id`: The preferred game-id for this room.
|
||||
- `-u/--username`: The username used for announce.
|
||||
- `-t/--token`: The token used for announce.
|
||||
- `-a/--web-api-url`: yuzu Web API url.
|
||||
- `-b/--ban-list-file`: The file for storing the room ban list.
|
||||
- `-l/--log-file`: The file for storing the room log.
|
||||
- `-h/--help`: Display this help and exit.
|
||||
- `-v/--version`: Output version information and exit.
|
||||
|
||||
Vendored
+30
@@ -106,6 +106,36 @@ endif()
|
||||
# SimpleIni
|
||||
AddJsonPackage(simpleini)
|
||||
|
||||
# Most linux distros don't package cubeb, so enable regardless of cpm settings
|
||||
if(ENABLE_CUBEB)
|
||||
AddJsonPackage(cubeb)
|
||||
|
||||
if (cubeb_ADDED)
|
||||
if (NOT MSVC)
|
||||
if (TARGET speex)
|
||||
target_compile_options(speex PRIVATE $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-sign-compare>)
|
||||
endif()
|
||||
set_target_properties(cubeb PROPERTIES COMPILE_OPTIONS "")
|
||||
target_compile_options(cubeb INTERFACE
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-implicit-const-int-float-conversion>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-shadow>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-missing-declarations>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-return-type>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-uninitialized>
|
||||
)
|
||||
else()
|
||||
target_compile_options(cubeb PRIVATE
|
||||
/wd4456
|
||||
/wd4458
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT TARGET cubeb::cubeb)
|
||||
add_library(cubeb::cubeb ALIAS cubeb)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT YUZU_USE_BUNDLED_SDL3)
|
||||
if (NOT WIN32)
|
||||
# Yuzu itself needs: Atomic Audio Events Joystick Haptic Sensor Threads Timers
|
||||
|
||||
@@ -15,7 +15,7 @@ pkgs.mkShellNoCC {
|
||||
enet libopus vulkan-headers vulkan-utility-libraries
|
||||
spirv-tools spirv-headers vulkan-loader unzip
|
||||
glslang python3 httplib cpp-jwt ffmpeg-headless
|
||||
libusb1
|
||||
libusb1 cubeb
|
||||
# eden
|
||||
qt6.qtbase qt6.qtmultimedia qt6.qtwayland qt6.qttools
|
||||
qt6.qtwebengine qt6.qt5compat
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RadioGroup
|
||||
import android.widget.TextView
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
@@ -673,6 +674,167 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFrameGenCell(inflater: LayoutInflater, row: ViewGroup): TextView {
|
||||
val cell = inflater.inflate(R.layout.item_quick_settings_frame_gen_cell, row, false)
|
||||
as TextView
|
||||
row.addView(cell)
|
||||
return cell
|
||||
}
|
||||
|
||||
fun addFrameGen(container: ViewGroup) {
|
||||
val inflater = LayoutInflater.from(emulationFragment.requireContext())
|
||||
val itemView = inflater.inflate(R.layout.item_quick_settings_frame_gen, container, false)
|
||||
|
||||
val multiplierRow = itemView.findViewById<LinearLayout>(R.id.frame_gen_multipliers)
|
||||
val targetRow = itemView.findViewById<LinearLayout>(R.id.frame_gen_targets)
|
||||
val targetSection = itemView.findViewById<ViewGroup>(R.id.frame_gen_target_section)
|
||||
val flowRow = itemView.findViewById<LinearLayout>(R.id.frame_gen_flow)
|
||||
val flowSection = itemView.findViewById<ViewGroup>(R.id.frame_gen_flow_section)
|
||||
|
||||
val multiplierNames =
|
||||
emulationFragment.resources.getStringArray(R.array.frameGenMultiplierNames)
|
||||
val multiplierValues =
|
||||
emulationFragment.resources.getIntArray(R.array.frameGenMultiplierValues)
|
||||
val targetValues =
|
||||
emulationFragment.resources.getIntArray(R.array.frameGenTargetRateValues)
|
||||
val flowValues =
|
||||
emulationFragment.resources.getIntArray(R.array.frameGenFlowScaleValues)
|
||||
|
||||
val columns = maxOf(
|
||||
multiplierValues.size + 1,
|
||||
targetValues.size,
|
||||
flowValues.size + 1
|
||||
).toFloat()
|
||||
multiplierRow.weightSum = columns
|
||||
targetRow.weightSum = columns
|
||||
flowRow.weightSum = columns
|
||||
|
||||
val powerCell = addFrameGenCell(inflater, multiplierRow)
|
||||
|
||||
val multiplierCells = mutableListOf<TextView>()
|
||||
for (name in multiplierNames) {
|
||||
val cell = addFrameGenCell(inflater, multiplierRow)
|
||||
cell.text = name
|
||||
multiplierCells.add(cell)
|
||||
}
|
||||
|
||||
val targetCells = mutableListOf<TextView>()
|
||||
for (value in targetValues) {
|
||||
val cell = addFrameGenCell(inflater, targetRow)
|
||||
if (value == 0) {
|
||||
cell.setText(R.string.frame_gen_fixed)
|
||||
} else {
|
||||
cell.text = value.toString()
|
||||
}
|
||||
targetCells.add(cell)
|
||||
}
|
||||
|
||||
val autoCell = addFrameGenCell(inflater, flowRow)
|
||||
autoCell.setText(R.string.frame_gen_flow_auto)
|
||||
|
||||
val flowCells = mutableListOf<TextView>()
|
||||
for (value in flowValues) {
|
||||
val cell = addFrameGenCell(inflater, flowRow)
|
||||
cell.text = "$value%"
|
||||
flowCells.add(cell)
|
||||
}
|
||||
|
||||
val inactiveAlpha = 0.38f
|
||||
|
||||
fun refresh() {
|
||||
val enabled = BooleanSetting.RENDERER_FRAME_GEN.getBoolean(needsGlobal = false)
|
||||
val multiplier = IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.getInt(needsGlobal = false)
|
||||
val target = IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.getInt(needsGlobal = false)
|
||||
val fixed = target == 0
|
||||
val flowAuto =
|
||||
BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.getBoolean(needsGlobal = false)
|
||||
val flowScale = IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.getInt(needsGlobal = false)
|
||||
|
||||
var powerLabel = R.string.frame_gen_off
|
||||
var rowAlpha = inactiveAlpha
|
||||
if (enabled) {
|
||||
powerLabel = R.string.frame_gen_on
|
||||
rowAlpha = 1.0f
|
||||
}
|
||||
|
||||
var multiplierAlpha = inactiveAlpha
|
||||
if (enabled && fixed) {
|
||||
multiplierAlpha = 1.0f
|
||||
}
|
||||
|
||||
powerCell.setText(powerLabel)
|
||||
powerCell.isSelected = enabled
|
||||
|
||||
multiplierCells.forEachIndexed { index, cell ->
|
||||
cell.isEnabled = enabled
|
||||
cell.isSelected = enabled && fixed && multiplierValues[index] == multiplier
|
||||
cell.alpha = multiplierAlpha
|
||||
}
|
||||
|
||||
targetSection.alpha = rowAlpha
|
||||
targetCells.forEachIndexed { index, cell ->
|
||||
cell.isEnabled = enabled
|
||||
cell.isSelected = enabled && targetValues[index] == target
|
||||
}
|
||||
|
||||
flowSection.alpha = rowAlpha
|
||||
autoCell.isEnabled = enabled
|
||||
autoCell.isSelected = enabled && flowAuto
|
||||
|
||||
flowCells.forEachIndexed { index, cell ->
|
||||
cell.isEnabled = enabled
|
||||
cell.isSelected = enabled && !flowAuto && flowValues[index] == flowScale
|
||||
}
|
||||
}
|
||||
|
||||
powerCell.setOnClickListener {
|
||||
if (BooleanSetting.RENDERER_FRAME_GEN.getBoolean(needsGlobal = false)) {
|
||||
BooleanSetting.RENDERER_FRAME_GEN.setBoolean(false)
|
||||
} else {
|
||||
IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.setInt(multiplierValues.first())
|
||||
IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.setInt(0)
|
||||
BooleanSetting.RENDERER_FRAME_GEN.setBoolean(true)
|
||||
}
|
||||
saveSettings()
|
||||
refresh()
|
||||
}
|
||||
|
||||
multiplierCells.forEachIndexed { index, cell ->
|
||||
cell.setOnClickListener {
|
||||
IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.setInt(multiplierValues[index])
|
||||
IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.setInt(0)
|
||||
saveSettings()
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
targetCells.forEachIndexed { index, cell ->
|
||||
cell.setOnClickListener {
|
||||
IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.setInt(targetValues[index])
|
||||
saveSettings()
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
autoCell.setOnClickListener {
|
||||
BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.setBoolean(true)
|
||||
saveSettings()
|
||||
refresh()
|
||||
}
|
||||
|
||||
flowCells.forEachIndexed { index, cell ->
|
||||
cell.setOnClickListener {
|
||||
IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.setInt(flowValues[index])
|
||||
BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.setBoolean(false)
|
||||
saveSettings()
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
refresh()
|
||||
container.addView(itemView)
|
||||
}
|
||||
|
||||
fun addDivider(container: ViewGroup) {
|
||||
val inflater = LayoutInflater.from(emulationFragment.requireContext())
|
||||
val dividerView = inflater.inflate(R.layout.item_quick_settings_divider, container, false)
|
||||
|
||||
-2
@@ -38,9 +38,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||
RENDERER_VERTEX_INPUT_DYNAMIC_STATE("vertex_input_dynamic_state"),
|
||||
RENDERER_SAMPLE_SHADING("sample_shading"),
|
||||
RENDERER_FRAME_GEN("frame_gen"),
|
||||
RENDERER_FRAME_GEN_FP16("frame_gen_fp16"),
|
||||
RENDERER_FRAME_GEN_FLOW_SCALE_AUTO("frame_gen_flow_scale_auto"),
|
||||
RENDERER_FRAME_GEN_DUMP_FLOW("frame_gen_dump_flow"),
|
||||
GPU_UNSWIZZLE_ENABLED("gpu_unswizzle_enabled"),
|
||||
PICTURE_IN_PICTURE("picture_in_picture"),
|
||||
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
||||
|
||||
+1
-17
@@ -123,9 +123,7 @@ abstract class SettingsItem(
|
||||
IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key,
|
||||
IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key,
|
||||
BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key,
|
||||
IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key,
|
||||
BooleanSetting.RENDERER_FRAME_GEN_FP16.key,
|
||||
BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW.key
|
||||
IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key
|
||||
)
|
||||
|
||||
const val TYPE_HEADER = 0
|
||||
@@ -709,20 +707,6 @@ abstract class SettingsItem(
|
||||
units = "%"
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.RENDERER_FRAME_GEN_FP16,
|
||||
titleId = R.string.frame_gen_fp16,
|
||||
descriptionId = R.string.frame_gen_fp16_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW,
|
||||
titleId = R.string.frame_gen_dump_flow,
|
||||
descriptionId = R.string.frame_gen_dump_flow_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.RENDERER_SCREEN_LAYOUT,
|
||||
|
||||
-2
@@ -124,7 +124,6 @@ class SettingsFragmentPresenter(
|
||||
) {
|
||||
add(IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key)
|
||||
}
|
||||
add(BooleanSetting.RENDERER_FRAME_GEN_FP16.key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1553,7 +1552,6 @@ class SettingsFragmentPresenter(
|
||||
add(BooleanSetting.DUMP_GUEST_SHADERS.key)
|
||||
add(BooleanSetting.GPU_LOG_SHADER_DUMPS.key)
|
||||
add(BooleanSetting.DUMP_MACROS.key)
|
||||
add(BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW.key)
|
||||
add(BooleanSetting.GPU_LOG_MEMORY_TRACKING.key)
|
||||
add(BooleanSetting.GPU_LOG_DRIVER_DEBUG.key)
|
||||
add(IntSetting.GPU_LOG_RING_BUFFER_SIZE.key)
|
||||
|
||||
@@ -92,6 +92,7 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||
import org.yuzu.yuzu_emu.utils.InputHandler
|
||||
import org.yuzu.yuzu_emu.utils.Log
|
||||
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import org.yuzu.yuzu_emu.utils.NativeFreedrenoConfig
|
||||
import org.yuzu.yuzu_emu.utils.NativePostProcessing
|
||||
@@ -1188,6 +1189,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
|
||||
quickSettings.addDivider(container)
|
||||
|
||||
if (LosslessScalingHelper.isInstalled() && LosslessScalingHelper.isSupportedByGpu()) {
|
||||
quickSettings.addFrameGen(container)
|
||||
quickSettings.addDivider(container)
|
||||
}
|
||||
|
||||
quickSettings.addIntSetting(
|
||||
R.string.renderer_accuracy,
|
||||
container,
|
||||
|
||||
@@ -58,6 +58,13 @@ class LicensesFragment : Fragment() {
|
||||
R.string.license_fidelityfx_fsr_copyright,
|
||||
R.string.license_fidelityfx_fsr_text
|
||||
),
|
||||
License(
|
||||
R.string.license_cubeb,
|
||||
R.string.license_cubeb_description,
|
||||
R.string.license_cubeb_link,
|
||||
R.string.license_cubeb_copyright,
|
||||
R.string.license_cubeb_text
|
||||
),
|
||||
License(
|
||||
R.string.license_dynarmic,
|
||||
R.string.license_dynarmic_description,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
@@ -37,7 +37,7 @@ class Game(
|
||||
|
||||
val settingsName: String
|
||||
get() {
|
||||
val programIdLong = programId.toLong()
|
||||
val programIdLong = programId.toLongOrNull() ?: 0L
|
||||
return if (programIdLong == 0L) {
|
||||
FileUtil.getFilename(Uri.parse(path))
|
||||
} else {
|
||||
@@ -47,7 +47,7 @@ class Game(
|
||||
|
||||
val programIdHex: String
|
||||
get() {
|
||||
val programIdLong = programId.toLong()
|
||||
val programIdLong = programId.toLongOrNull() ?: 0L
|
||||
return if (programIdLong == 0L) {
|
||||
"0"
|
||||
} else {
|
||||
|
||||
@@ -1168,7 +1168,7 @@ VkPhysicalDeviceProperties GetVulkanDeviceProperties() {
|
||||
return physical_device.GetProperties();
|
||||
}
|
||||
|
||||
bool GetVulkanMemoryModelSupport() {
|
||||
bool GetFrameGenerationSupport() {
|
||||
Common::DynamicLibrary library;
|
||||
if (!library.Open("libvulkan.so")) {
|
||||
return false;
|
||||
@@ -1183,9 +1183,13 @@ bool GetVulkanMemoryModelSupport() {
|
||||
|
||||
const Vulkan::vk::PhysicalDevice physical_device(physical_devices[0], dld);
|
||||
|
||||
VkPhysicalDeviceShaderFloat16Int8Features float16_int8{
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT16_INT8_FEATURES,
|
||||
.pNext = nullptr,
|
||||
};
|
||||
VkPhysicalDeviceVulkanMemoryModelFeatures memory_model{
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES,
|
||||
.pNext = nullptr,
|
||||
.pNext = &float16_int8,
|
||||
};
|
||||
VkPhysicalDeviceFeatures2 features{
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
|
||||
@@ -1193,7 +1197,7 @@ bool GetVulkanMemoryModelSupport() {
|
||||
};
|
||||
physical_device.GetFeatures2(features);
|
||||
|
||||
return memory_model.vulkanMemoryModel == VK_TRUE;
|
||||
return memory_model.vulkanMemoryModel == VK_TRUE && float16_int8.shaderFloat16 == VK_TRUE;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -1272,7 +1276,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getVulkanApiVersion(JNIEnv* env, j
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_supportsFrameGeneration(JNIEnv* env, jobject jobj) {
|
||||
try {
|
||||
return static_cast<jboolean>(GetVulkanMemoryModelSupport());
|
||||
return static_cast<jboolean>(GetFrameGenerationSupport());
|
||||
} catch (...) {
|
||||
return static_cast<jboolean>(false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorOnPrimary" android:state_selected="true" />
|
||||
<item android:color="?attr/colorOnSurface" />
|
||||
</selector>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?attr/colorControlHighlight">
|
||||
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/white" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<selector>
|
||||
<item android:state_selected="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?attr/colorPrimary" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="8dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="?attr/colorOutline" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
|
||||
</ripple>
|
||||
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="18dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/frame_gen" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/frame_gen_quick_description"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/frame_gen_multipliers"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:orientation="horizontal" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/frame_gen_target_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="18dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/frame_gen_target_rate" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/frame_gen_target_rate_quick_description"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/frame_gen_targets"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:orientation="horizontal" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/frame_gen_flow_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="18dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/frame_gen_flow_scale" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/frame_gen_flow_scale_quick_description"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/frame_gen_flow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:orientation="horizontal" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:background="@drawable/frame_gen_cell_background"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/frame_gen_cell_text" />
|
||||
@@ -305,8 +305,6 @@
|
||||
<string name="frame_gen_target_rate_60">60 إطارًا في الثانية</string>
|
||||
<string name="frame_gen_target_rate_90">90 إطارًا في الثانية</string>
|
||||
<string name="frame_gen_target_rate_120">120 إطارًا في الثانية</string>
|
||||
<string name="frame_gen_target_rate_144">144 إطارًا في الثانية</string>
|
||||
<string name="frame_gen_target_rate_165">165 إطارًا في الثانية</string>
|
||||
<string name="frame_gen_queue_target">هدف قائمة انتظار الإطارات</string>
|
||||
<string name="frame_gen_queue_target_description">كم عدد الإطارات المكتملة التي قد تنتظر قبل عرضها؟ تعمل قوائم الانتظار الأكبر حجمًا على امتصاص الارتفاعات المفاجئة في حمل وحدة معالجة الرسومات على حساب زمن انتقال الإدخال.</string>
|
||||
<string name="frame_gen_queue_target_0">أقل زمن انتقال (بدون تخزين مؤقت)</string>
|
||||
@@ -316,10 +314,6 @@
|
||||
<string name="frame_gen_flow_scale_auto_description">قم بتقدير الحركة بناءً على الدقة التي تعرضها اللعبة فعليًّا، بدلاً من الإخراج الذي تم رفع دقته. ولا يؤثر ذلك على الدقة بأي شكل، لأن رفع الدقة لا يضيف أي تفاصيل تتعلق بالحركة.</string>
|
||||
<string name="frame_gen_flow_scale">دقة تقدير الحركة</string>
|
||||
<string name="frame_gen_flow_scale_description">دقة مسار التدفق البصري، كجزء من الناتج. ويُعد خفض هذه القيمة أرخص طريقة لاستعادة الأداء.</string>
|
||||
<string name="frame_gen_fp16">مُظلِّلات نصف الدقة</string>
|
||||
<string name="frame_gen_fp16_description">استخدم نسخة التظليل 16 بت. يتم التراجع تلقائيًا إلى الخيار البديل في حالة عدم توفرها في برنامج التشغيل أو الملف.</string>
|
||||
<string name="frame_gen_dump_flow">إفراغ الإطار الذي تم إنشاؤه</string>
|
||||
<string name="frame_gen_dump_flow_description">قم بكتابة مستويات MIP للتدفق البصري والإطار المُستكمل إلى مجلد lossless/debug مرة واحدة، لغرض استكشاف الأخطاء وإصلاحها</string>
|
||||
<string name="frame_gen_unsupported">توليد الإطار غير متاح</string>
|
||||
<string name="frame_gen_unsupported_description">لا يدعم برنامج تشغيل وحدة معالجة الرسومات هذا نموذج ذاكرة Vulkan، الذي تتطلبه برامج التظليل الخاصة بـ«Lossless Scaling».</string>
|
||||
<string name="lossless_scaling_setup_description">اختياري. قم بتوفير ملف Lossless.dll الخاص بك لتمكين إنشاء الإطارات لاحقًا</string>
|
||||
|
||||
@@ -304,8 +304,6 @@
|
||||
<string name="frame_gen_target_rate_60">60 FPS</string>
|
||||
<string name="frame_gen_target_rate_90">90 FPS</string>
|
||||
<string name="frame_gen_target_rate_120">120 FPS</string>
|
||||
<string name="frame_gen_target_rate_144">144 FPS</string>
|
||||
<string name="frame_gen_target_rate_165">165 FPS</string>
|
||||
<string name="frame_gen_queue_target_1">Ausbalanciert (1 Bild)</string>
|
||||
<string name="frame_gen_queue_target_2">Flüssigste (2 Bilder)</string>
|
||||
<string name="frame_gen_unsupported">Frame-Generation nicht verfügbar</string>
|
||||
|
||||
@@ -301,12 +301,9 @@
|
||||
<string name="frame_gen_target_rate_60">60 FPS</string>
|
||||
<string name="frame_gen_target_rate_90">90 FPS</string>
|
||||
<string name="frame_gen_target_rate_120">120 FPS</string>
|
||||
<string name="frame_gen_target_rate_144">144 FPS</string>
|
||||
<string name="frame_gen_target_rate_165">165 FPS</string>
|
||||
<string name="frame_gen_queue_target_0">Latencia más baja (Sin búfer)</string>
|
||||
<string name="frame_gen_queue_target_1">Equilibrado (1 fotograma)</string>
|
||||
<string name="frame_gen_queue_target_2">Más suave (2 fotogramas)</string>
|
||||
<string name="frame_gen_fp16">Sombreadores de media precisión</string>
|
||||
<string name="frame_gen_unsupported">Generación de fotogramas no disponbile</string>
|
||||
<string name="lossless_scaling_install">Instalar Lossless.dll</string>
|
||||
<string name="lossless_scaling_replace_description">Seleccionar una copia diferente de Lossless.dll</string>
|
||||
|
||||
@@ -799,6 +799,9 @@
|
||||
<string name="theme_mode_light">روشن</string>
|
||||
<string name="theme_mode_dark">تاریک</string>
|
||||
|
||||
<!-- Audio output engines -->
|
||||
<string name="cubeb">cubeb</string>
|
||||
|
||||
<!-- Anisotropic filtering options -->
|
||||
<string name="multiplier_x2">x2</string>
|
||||
<string name="multiplier_x4">x4</string>
|
||||
|
||||
@@ -308,10 +308,6 @@
|
||||
<string name="frame_gen_flow_scale_auto_description">Оценивать движение в разрешении, которое игра действительно рендерит, вместо масштабированного вывода. Ничего не стоит в точности, так как масштабирование не добавляет деталей движения.</string>
|
||||
<string name="frame_gen_flow_scale">Разрешение оценки движения</string>
|
||||
<string name="frame_gen_flow_scale_description">Разрешение прохода оптического потока в долях от выходного разрешения. Его понижение — самый дешёвый способ вернуть производительность.</string>
|
||||
<string name="frame_gen_fp16">Шейдеры половинной точности</string>
|
||||
<string name="frame_gen_fp16_description">Использовать 16-битную версию шейдеров. Автоматически переключается на обычную, если драйвер или файл не поддерживают её.</string>
|
||||
<string name="frame_gen_dump_flow">Сохранить сгенерированный кадр</string>
|
||||
<string name="frame_gen_dump_flow_description">Однократно записать уровни мип-карт оптического потока и интерполированный кадр в папку lossless/debug для диагностики.</string>
|
||||
<string name="frame_gen_unsupported">Генерация кадров недоступна</string>
|
||||
<string name="frame_gen_unsupported_description">Драйвер ГПУ не поддерживает модель памяти Vulkan, требуемую шейдерами Lossless Scaling.</string>
|
||||
<string name="lossless_scaling_setup_description">Опционально. Укажите свой Lossless.dll для включения генерации кадров позже.</string>
|
||||
|
||||
@@ -305,8 +305,6 @@
|
||||
<string name="frame_gen_target_rate_60">60 FPS</string>
|
||||
<string name="frame_gen_target_rate_90">90 FPS</string>
|
||||
<string name="frame_gen_target_rate_120">120 FPS</string>
|
||||
<string name="frame_gen_target_rate_144">144 FPS</string>
|
||||
<string name="frame_gen_target_rate_165">165 FPS</string>
|
||||
<string name="frame_gen_queue_target">帧队列目标</string>
|
||||
<string name="frame_gen_queue_target_description">在显示之前有多少已完成渲染的帧正在等待。较大的队列可以缓解 GPU 突发的压力,但会增加输入延迟。</string>
|
||||
<string name="frame_gen_queue_target_0">最低延迟 (无缓冲)</string>
|
||||
@@ -316,10 +314,6 @@
|
||||
<string name="frame_gen_flow_scale_auto_description">在游戏实际渲染的分辨率下估算运动,而不是在放大后的输出上。不会影响精确性,因为放大并不会增加运动细节。</string>
|
||||
<string name="frame_gen_flow_scale">运动预估分辨率</string>
|
||||
<string name="frame_gen_flow_scale_description">光流通道的分辨率,以输出的比例表示。降低它是提升性能最经济的做法。</string>
|
||||
<string name="frame_gen_fp16">半精度着色器</string>
|
||||
<string name="frame_gen_fp16_description">使用 16 位着色器变体。如果驱动或文件不支持会自动回退。</string>
|
||||
<string name="frame_gen_dump_flow">转储已生成的帧</string>
|
||||
<string name="frame_gen_dump_flow_description">为了排查问题,把光流 mip 级别和插值帧写入 lossless/debug 文件夹一次</string>
|
||||
<string name="frame_gen_unsupported">帧生成不可用</string>
|
||||
<string name="frame_gen_unsupported_description">这个 GPU 驱动不支持无损缩放着色器所需的 Vulkan 内存模型。</string>
|
||||
<string name="lossless_scaling_setup_description">作为可选项。请提供您自己的 Lossless.dll 以便在之后可以启用帧生成。</string>
|
||||
|
||||
@@ -305,8 +305,6 @@
|
||||
<string name="frame_gen_target_rate_60">60 FPS</string>
|
||||
<string name="frame_gen_target_rate_90">90 FPS</string>
|
||||
<string name="frame_gen_target_rate_120">120 FPS</string>
|
||||
<string name="frame_gen_target_rate_144">144 FPS</string>
|
||||
<string name="frame_gen_target_rate_165">165 FPS</string>
|
||||
<string name="frame_gen_queue_target">影格佇列目標</string>
|
||||
<string name="frame_gen_queue_target_description">最多允許多少個已完成的影格在顯示器前等待,較大的佇列可以吸收 GPU 突發負載,但會增加輸入延遲</string>
|
||||
<string name="frame_gen_queue_target_0">最低延遲(無緩衝)</string>
|
||||
@@ -316,10 +314,6 @@
|
||||
<string name="frame_gen_flow_scale_auto_description">以遊戲實際渲染的解析度而非升頻後的輸出進行運動預測。由於升頻後不會增加任何動態細節,因此不會影響準確度</string>
|
||||
<string name="frame_gen_flow_scale">運動預測解析度</string>
|
||||
<string name="frame_gen_flow_scale_description">光流處理的解析度,以輸出解析度的比例表示。降低此設定值是減少效能負載最有效的方法</string>
|
||||
<string name="frame_gen_fp16">半準確著色器</string>
|
||||
<string name="frame_gen_fp16_description">使用16位元著色器。如果驅動程式或著色器檔案不支援則會自動切換成其它版本</string>
|
||||
<string name="frame_gen_dump_flow">傾印生成的著色器</string>
|
||||
<string name="frame_gen_dump_flow_description">將光流的 MIP 層級與補間影格寫入 Eden 資料夾中的 lossless\debug 資料夾以便進行疑難排解</string>
|
||||
<string name="frame_gen_unsupported">無法使用影格生成</string>
|
||||
<string name="frame_gen_unsupported_description">Lossless Scaling 的著色器需要 Vulkan 記憶體模型,所選的驅動程式不支援該功能</string>
|
||||
<string name="lossless_scaling_setup_description">可選擇安裝自己擁有的 Lossless.dll 以在之後啟用影格生成功能</string>
|
||||
|
||||
@@ -174,8 +174,6 @@
|
||||
<item>@string/frame_gen_target_rate_60</item>
|
||||
<item>@string/frame_gen_target_rate_90</item>
|
||||
<item>@string/frame_gen_target_rate_120</item>
|
||||
<item>@string/frame_gen_target_rate_144</item>
|
||||
<item>@string/frame_gen_target_rate_165</item>
|
||||
</string-array>
|
||||
|
||||
<integer-array name="frameGenTargetRateValues">
|
||||
@@ -183,8 +181,12 @@
|
||||
<item>60</item>
|
||||
<item>90</item>
|
||||
<item>120</item>
|
||||
<item>144</item>
|
||||
<item>165</item>
|
||||
</integer-array>
|
||||
|
||||
<integer-array name="frameGenFlowScaleValues">
|
||||
<item>50</item>
|
||||
<item>75</item>
|
||||
<item>100</item>
|
||||
</integer-array>
|
||||
|
||||
<string-array name="frameGenQueueTargetNames">
|
||||
@@ -466,11 +468,13 @@
|
||||
<string-array name="outputEngineEntries">
|
||||
<item>@string/auto</item>
|
||||
<item>@string/sdl3</item>
|
||||
<item>@string/cubeb</item>
|
||||
<item>@string/string_null</item>
|
||||
</string-array>
|
||||
<integer-array name="outputEngineValues">
|
||||
<item>0</item>
|
||||
<item>2</item>
|
||||
<item>1</item>
|
||||
<item>3</item>
|
||||
</integer-array>
|
||||
|
||||
|
||||
@@ -331,8 +331,13 @@
|
||||
<string name="frame_gen_target_rate_60">60 FPS</string>
|
||||
<string name="frame_gen_target_rate_90">90 FPS</string>
|
||||
<string name="frame_gen_target_rate_120">120 FPS</string>
|
||||
<string name="frame_gen_target_rate_144">144 FPS</string>
|
||||
<string name="frame_gen_target_rate_165">165 FPS</string>
|
||||
<string name="frame_gen_on">On</string>
|
||||
<string name="frame_gen_off">Off</string>
|
||||
<string name="frame_gen_fixed">Fixed</string>
|
||||
<string name="frame_gen_flow_auto">Auto</string>
|
||||
<string name="frame_gen_quick_description">Turn frame generation on or off and choose the frame multiplier.</string>
|
||||
<string name="frame_gen_target_rate_quick_description">Generate frames up to a target frame rate. The multiplier adjusts on its own.</string>
|
||||
<string name="frame_gen_flow_scale_quick_description">Resolution used to estimate motion between frames. Lower values save GPU time.</string>
|
||||
<string name="frame_gen_queue_target">Frame queue target</string>
|
||||
<string name="frame_gen_queue_target_description">How many finished frames may wait ahead of the display. Larger queues absorb GPU spikes at the cost of input latency.</string>
|
||||
<string name="frame_gen_queue_target_0">Lowest latency (Unbuffered)</string>
|
||||
@@ -342,19 +347,15 @@
|
||||
<string name="frame_gen_flow_scale_auto_description">Estimate motion at the resolution the game actually renders instead of the upscaled output. Costs nothing in accuracy, since upscaling adds no motion detail.</string>
|
||||
<string name="frame_gen_flow_scale">Motion estimation resolution</string>
|
||||
<string name="frame_gen_flow_scale_description">Resolution of the optical flow pass, as a fraction of the output. Lowering it is the cheapest way to reclaim performance.</string>
|
||||
<string name="frame_gen_fp16">Half precision shaders</string>
|
||||
<string name="frame_gen_fp16_description">Use the 16-bit shader variant. Falls back automatically if the driver or the file lacks it.</string>
|
||||
<string name="frame_gen_dump_flow">Dump generated frame</string>
|
||||
<string name="frame_gen_dump_flow_description">Write the optical flow mip levels and the interpolated frame to the lossless/debug folder once, for troubleshooting</string>
|
||||
<string name="frame_gen_unsupported">Frame generation unavailable</string>
|
||||
<string name="frame_gen_unsupported_description">This GPU driver does not support the Vulkan memory model, which the Lossless Scaling shaders require.</string>
|
||||
<string name="frame_gen_unsupported_description">This GPU driver lacks the Vulkan memory model or half precision (float16) support that the Lossless Scaling shaders require.</string>
|
||||
<string name="lossless_scaling_setup_description">Optional. Provide your own Lossless.dll to enable frame generation later</string>
|
||||
<string name="lossless_scaling_install">Install Lossless.dll</string>
|
||||
<string name="lossless_scaling_install_description">Frame generation needs your own legal copy of Lossless.dll from Lossless Scaling</string>
|
||||
<string name="lossless_scaling_replace_description">Select a different copy of Lossless.dll</string>
|
||||
<string name="frame_generation_support">Frame generation</string>
|
||||
<string name="frame_generation_supported">Supported</string>
|
||||
<string name="frame_generation_unsupported">Unsupported (no Vulkan memory model)</string>
|
||||
<string name="frame_generation_unsupported">Unsupported (no Vulkan memory model or float16)</string>
|
||||
<string name="lossless_scaling">Lossless Scaling</string>
|
||||
<string name="lossless_scaling_description">Provide your own copy of Lossless.dll to enable frame generation</string>
|
||||
<string name="lossless_scaling_installed">Installed</string>
|
||||
@@ -1262,6 +1263,7 @@
|
||||
|
||||
<!-- Audio output engines -->
|
||||
<string name="sdl3" translatable="false">SDL3</string>
|
||||
<string name="cubeb" translatable="false">cubeb</string>
|
||||
|
||||
<!-- Anisotropic filtering options -->
|
||||
<string name="multiplier_x2" translatable="false">x2</string>
|
||||
@@ -1401,6 +1403,23 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
</string>
|
||||
<string name="license_cubeb" translatable="false">cubeb</string>
|
||||
<string name="license_cubeb_description" translatable="false">Cross platform audio library</string>
|
||||
<string name="license_cubeb_link" translatable="false">https://github.com/mozilla/cubeb</string>
|
||||
<string name="license_cubeb_copyright" translatable="false">Copyright © 2011 Mozilla Foundation</string>
|
||||
<string name="license_cubeb_text" translatable="false">
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.\n\n
|
||||
|
||||
THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
</string>
|
||||
<string name="license_dynarmic" translatable="false">Dynarmic</string>
|
||||
<string name="license_dynarmic_description" translatable="false">An ARM dynamic recompiler</string>
|
||||
|
||||
@@ -229,6 +229,15 @@ endif()
|
||||
target_include_directories(audio_core PRIVATE ${OPUS_INCLUDE_DIRS})
|
||||
target_link_libraries(audio_core PUBLIC common core Opus::opus)
|
||||
|
||||
if (ENABLE_CUBEB)
|
||||
target_sources(audio_core PRIVATE
|
||||
sink/cubeb_sink.cpp
|
||||
sink/cubeb_sink.h)
|
||||
|
||||
target_link_libraries(audio_core PRIVATE cubeb::cubeb)
|
||||
target_compile_definitions(audio_core PRIVATE HAVE_CUBEB=1)
|
||||
endif()
|
||||
|
||||
target_sources(audio_core PRIVATE
|
||||
sink/sdl3_sink.cpp
|
||||
sink/sdl3_sink.h)
|
||||
@@ -236,8 +245,4 @@ target_sources(audio_core PRIVATE
|
||||
target_link_libraries(audio_core PRIVATE SDL3::SDL3)
|
||||
target_compile_definitions(audio_core PRIVATE HAVE_SDL3)
|
||||
|
||||
if (ANDROID)
|
||||
target_link_libraries(audio_core PRIVATE OpenSLES)
|
||||
endif()
|
||||
|
||||
create_target_directory_groups(audio_core)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "audio_core/common/common.h"
|
||||
#include "audio_core/sink/cubeb_sink.h"
|
||||
#include "audio_core/sink/sink_stream.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/scope_exit.h"
|
||||
#include "core/core.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <objbase.h>
|
||||
#undef CreateEvent
|
||||
#endif
|
||||
|
||||
namespace AudioCore::Sink {
|
||||
/**
|
||||
* Cubeb sink stream, responsible for sinking samples to hardware.
|
||||
*/
|
||||
class CubebSinkStream final : public SinkStream {
|
||||
public:
|
||||
/**
|
||||
* Create a new sink stream.
|
||||
*
|
||||
* @param ctx_ - Cubeb context to create this stream with.
|
||||
* @param device_channels_ - Number of channels supported by the hardware.
|
||||
* @param system_channels_ - Number of channels the audio systems expect.
|
||||
* @param output_device - Cubeb output device id.
|
||||
* @param input_device - Cubeb input device id.
|
||||
* @param name_ - Name of this stream.
|
||||
* @param type_ - Type of this stream.
|
||||
* @param system_ - Core system.
|
||||
* @param event - Event used only for audio renderer, signalled on buffer consume.
|
||||
*/
|
||||
CubebSinkStream(cubeb* ctx_, u32 device_channels_, u32 system_channels_,
|
||||
cubeb_devid output_device, cubeb_devid input_device, const std::string& name_,
|
||||
StreamType type_, Core::System& system_)
|
||||
: SinkStream(system_, type_), ctx{ctx_} {
|
||||
#ifdef _WIN32
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
#endif
|
||||
name = name_;
|
||||
device_channels = device_channels_;
|
||||
system_channels = system_channels_;
|
||||
|
||||
cubeb_stream_params params{};
|
||||
params.rate = TargetSampleRate;
|
||||
params.channels = device_channels;
|
||||
params.format = CUBEB_SAMPLE_S16LE;
|
||||
params.prefs = CUBEB_STREAM_PREF_NONE;
|
||||
switch (params.channels) {
|
||||
case 1:
|
||||
params.layout = CUBEB_LAYOUT_MONO;
|
||||
break;
|
||||
case 2:
|
||||
params.layout = CUBEB_LAYOUT_STEREO;
|
||||
break;
|
||||
case 6:
|
||||
params.layout = CUBEB_LAYOUT_3F2_LFE;
|
||||
break;
|
||||
}
|
||||
|
||||
u32 minimum_latency{0};
|
||||
const auto latency_error = cubeb_get_min_latency(ctx, ¶ms, &minimum_latency);
|
||||
if (latency_error != CUBEB_OK) {
|
||||
LOG_CRITICAL(Audio_Sink, "Error getting minimum latency, error: {}", latency_error);
|
||||
minimum_latency = TargetSampleCount * 2;
|
||||
}
|
||||
|
||||
minimum_latency = (std::max)(minimum_latency, TargetSampleCount * 2);
|
||||
|
||||
LOG_INFO(Service_Audio,
|
||||
"Opening cubeb stream {} type {} with: rate {} channels {} (system channels {}) "
|
||||
"latency {}",
|
||||
name, type, params.rate, params.channels, system_channels, minimum_latency);
|
||||
|
||||
auto init_error{0};
|
||||
if (type == StreamType::In) {
|
||||
init_error = cubeb_stream_init(ctx, &stream_backend, name.c_str(), input_device,
|
||||
¶ms, output_device, nullptr, minimum_latency,
|
||||
&CubebSinkStream::DataCallback,
|
||||
&CubebSinkStream::StateCallback, this);
|
||||
} else {
|
||||
init_error = cubeb_stream_init(ctx, &stream_backend, name.c_str(), input_device,
|
||||
nullptr, output_device, ¶ms, minimum_latency,
|
||||
&CubebSinkStream::DataCallback,
|
||||
&CubebSinkStream::StateCallback, this);
|
||||
}
|
||||
|
||||
if (init_error != CUBEB_OK) {
|
||||
LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream, error: {}", init_error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the sink stream.
|
||||
*/
|
||||
~CubebSinkStream() override {
|
||||
LOG_DEBUG(Service_Audio, "Destructing cubeb stream {}", name);
|
||||
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
Finalize();
|
||||
|
||||
#ifdef _WIN32
|
||||
CoUninitialize();
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize the sink stream.
|
||||
*/
|
||||
void Finalize() override {
|
||||
Stop();
|
||||
cubeb_stream_destroy(stream_backend);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the sink stream.
|
||||
*
|
||||
* @param resume - Set to true if this is resuming the stream a previously-active stream.
|
||||
* Default false.
|
||||
*/
|
||||
void Start(bool resume = false) override {
|
||||
if (!ctx || !paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
paused = false;
|
||||
if (cubeb_stream_start(stream_backend) != CUBEB_OK) {
|
||||
LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the sink stream.
|
||||
*/
|
||||
void Stop() override {
|
||||
if (!ctx || paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
SignalPause();
|
||||
if (cubeb_stream_stop(stream_backend) != CUBEB_OK) {
|
||||
LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream");
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* Main callback from Cubeb. Either expects samples from us (audio render/audio out), or will
|
||||
* provide samples to be copied (audio in).
|
||||
*
|
||||
* @param stream - Cubeb-specific data about the stream.
|
||||
* @param user_data - Custom data pointer passed along, points to a CubebSinkStream.
|
||||
* @param in_buff - Input buffer to be used if the stream is an input type.
|
||||
* @param out_buff - Output buffer to be used if the stream is an output type.
|
||||
* @param num_frames_ - Number of frames of audio in the buffers. Note: Not number of samples.
|
||||
*/
|
||||
static long DataCallback([[maybe_unused]] cubeb_stream* stream, void* user_data,
|
||||
[[maybe_unused]] const void* in_buff, void* out_buff,
|
||||
long num_frames_) {
|
||||
auto* impl = static_cast<CubebSinkStream*>(user_data);
|
||||
if (!impl) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const std::size_t num_channels = impl->GetDeviceChannels();
|
||||
const std::size_t frame_size = num_channels;
|
||||
const std::size_t num_frames{static_cast<size_t>(num_frames_)};
|
||||
|
||||
if (impl->type == StreamType::In) {
|
||||
std::span<const s16> input_buffer{reinterpret_cast<const s16*>(in_buff),
|
||||
num_frames * frame_size};
|
||||
impl->ProcessAudioIn(input_buffer, num_frames);
|
||||
} else {
|
||||
std::span<s16> output_buffer{reinterpret_cast<s16*>(out_buff), num_frames * frame_size};
|
||||
impl->ProcessAudioOutAndRender(output_buffer, num_frames);
|
||||
}
|
||||
|
||||
return num_frames_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cubeb callback for if a device state changes. Unused currently.
|
||||
*
|
||||
* @param stream - Cubeb-specific data about the stream.
|
||||
* @param user_data - Custom data pointer passed along, points to a CubebSinkStream.
|
||||
* @param state - New state of the device.
|
||||
*/
|
||||
static void StateCallback(cubeb_stream*, void*, cubeb_state) {}
|
||||
|
||||
/// Main Cubeb context
|
||||
cubeb* ctx{};
|
||||
/// Cubeb stream backend
|
||||
cubeb_stream* stream_backend{};
|
||||
};
|
||||
|
||||
CubebSink::CubebSink(std::string_view target_device_name) {
|
||||
// Cubeb requires COM to be initialized on the thread calling cubeb_init on Windows
|
||||
#ifdef _WIN32
|
||||
com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
#endif
|
||||
|
||||
if (cubeb_init(&ctx, "Eden", nullptr) != CUBEB_OK) {
|
||||
LOG_CRITICAL(Audio_Sink, "cubeb_init failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (target_device_name != auto_device_name && !target_device_name.empty()) {
|
||||
cubeb_device_collection collection;
|
||||
if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) {
|
||||
LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported");
|
||||
} else {
|
||||
const auto collection_end{collection.device + collection.count};
|
||||
const auto device{
|
||||
std::find_if(collection.device, collection_end, [&](const cubeb_device_info& info) {
|
||||
return info.friendly_name != nullptr &&
|
||||
target_device_name == std::string(info.friendly_name);
|
||||
})};
|
||||
if (device != collection_end) {
|
||||
output_device = device->devid;
|
||||
}
|
||||
cubeb_device_collection_destroy(ctx, &collection);
|
||||
}
|
||||
}
|
||||
|
||||
cubeb_get_max_channel_count(ctx, &device_channels);
|
||||
device_channels = device_channels >= 6U ? 6U : 2U;
|
||||
}
|
||||
|
||||
CubebSink::~CubebSink() {
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& sink_stream : sink_streams) {
|
||||
sink_stream.reset();
|
||||
}
|
||||
|
||||
cubeb_destroy(ctx);
|
||||
|
||||
#ifdef _WIN32
|
||||
if (SUCCEEDED(com_init_result)) {
|
||||
CoUninitialize();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SinkStream* CubebSink::AcquireSinkStream(Core::System& system, u32 system_channels_,
|
||||
const std::string& name, StreamType type) {
|
||||
system_channels = system_channels_;
|
||||
SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<CubebSinkStream>(
|
||||
ctx, device_channels, system_channels, output_device, input_device, name, type, system));
|
||||
stream->SetDeviceVolume(device_volume);
|
||||
|
||||
return stream.get();
|
||||
}
|
||||
|
||||
void CubebSink::CloseStream(SinkStream* stream) {
|
||||
for (size_t i = 0; i < sink_streams.size(); i++) {
|
||||
if (sink_streams[i].get() == stream) {
|
||||
sink_streams[i].reset();
|
||||
sink_streams.erase(sink_streams.begin() + i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CubebSink::CloseStreams() {
|
||||
sink_streams.clear();
|
||||
}
|
||||
|
||||
f32 CubebSink::GetDeviceVolume() const {
|
||||
return device_volume;
|
||||
}
|
||||
|
||||
void CubebSink::SetDeviceVolume(f32 volume) {
|
||||
device_volume = volume;
|
||||
for (auto& stream : sink_streams) {
|
||||
stream->SetDeviceVolume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
void CubebSink::SetSystemVolume(f32 volume) {
|
||||
for (auto& stream : sink_streams) {
|
||||
stream->SetSystemVolume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ListCubebSinkDevices(bool capture) {
|
||||
std::vector<std::string> device_list;
|
||||
cubeb* ctx;
|
||||
|
||||
#ifdef _WIN32
|
||||
auto com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
#endif
|
||||
|
||||
if (cubeb_init(&ctx, "Eden Device Enumerator", nullptr) != CUBEB_OK) {
|
||||
LOG_CRITICAL(Audio_Sink, "cubeb_init failed");
|
||||
return {};
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (SUCCEEDED(com_init_result)) {
|
||||
CoUninitialize();
|
||||
}
|
||||
#endif
|
||||
|
||||
auto type{capture ? CUBEB_DEVICE_TYPE_INPUT : CUBEB_DEVICE_TYPE_OUTPUT};
|
||||
cubeb_device_collection collection;
|
||||
if (cubeb_enumerate_devices(ctx, type, &collection) != CUBEB_OK) {
|
||||
LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported");
|
||||
} else {
|
||||
for (std::size_t i = 0; i < collection.count; i++) {
|
||||
const cubeb_device_info& device = collection.device[i];
|
||||
if (device.friendly_name && device.friendly_name[0] != '\0' &&
|
||||
device.state == CUBEB_DEVICE_STATE_ENABLED) {
|
||||
device_list.emplace_back(device.friendly_name);
|
||||
}
|
||||
}
|
||||
cubeb_device_collection_destroy(ctx, &collection);
|
||||
}
|
||||
|
||||
cubeb_destroy(ctx);
|
||||
return device_list;
|
||||
}
|
||||
|
||||
/* REVERSION TO 3833 - function GetCubebLatency REINTRODUCED FROM 3833 - DIABLO 3 FIX */
|
||||
u32 GetCubebLatency() {
|
||||
cubeb* ctx;
|
||||
|
||||
#ifdef _WIN32
|
||||
auto com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
#endif
|
||||
|
||||
// Init cubeb
|
||||
if (cubeb_init(&ctx, "yuzu Latency Getter", nullptr) != CUBEB_OK) {
|
||||
LOG_CRITICAL(Audio_Sink, "cubeb_init failed");
|
||||
// Return a large latency so we choose SDL instead.
|
||||
return 10000u;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (SUCCEEDED(com_init_result)) {
|
||||
CoUninitialize();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Get min latency
|
||||
cubeb_stream_params params{};
|
||||
params.rate = TargetSampleRate;
|
||||
params.channels = 2;
|
||||
params.format = CUBEB_SAMPLE_S16LE;
|
||||
params.prefs = CUBEB_STREAM_PREF_NONE;
|
||||
params.layout = CUBEB_LAYOUT_STEREO;
|
||||
|
||||
u32 latency{0};
|
||||
const auto latency_error = cubeb_get_min_latency(ctx, ¶ms, &latency);
|
||||
if (latency_error != CUBEB_OK) {
|
||||
LOG_CRITICAL(Audio_Sink, "Error getting minimum latency, error: {}", latency_error);
|
||||
latency = TargetSampleCount * 2;
|
||||
}
|
||||
latency = (std::max)(latency, TargetSampleCount * 2);
|
||||
cubeb_destroy(ctx);
|
||||
return latency;
|
||||
}
|
||||
|
||||
// REVERTED back to 3833 - Below namespace section and function IsCubebSuitable() removed, reverting to GetCubebLatency() above. - DIABLO 3 FIX
|
||||
/*
|
||||
namespace {
|
||||
static long TmpDataCallback(cubeb_stream*, void*, const void*, void*, long) {
|
||||
return TargetSampleCount;
|
||||
}
|
||||
static void TmpStateCallback(cubeb_stream*, void*, cubeb_state) {}
|
||||
} // namespace
|
||||
|
||||
bool IsCubebSuitable() {
|
||||
#if !defined(HAVE_CUBEB)
|
||||
return false;
|
||||
#else
|
||||
cubeb* ctx{nullptr};
|
||||
|
||||
#ifdef _WIN32
|
||||
auto com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
#endif
|
||||
|
||||
// Init cubeb
|
||||
if (cubeb_init(&ctx, "Eden Latency Getter", nullptr) != CUBEB_OK) {
|
||||
LOG_ERROR(Audio_Sink, "Cubeb failed to init, it is not suitable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
SCOPE_EXIT {
|
||||
cubeb_destroy(ctx);
|
||||
};
|
||||
|
||||
#ifdef _WIN32
|
||||
if (SUCCEEDED(com_init_result)) {
|
||||
CoUninitialize();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Get min latency
|
||||
cubeb_stream_params params{};
|
||||
params.rate = TargetSampleRate;
|
||||
params.channels = 2;
|
||||
params.format = CUBEB_SAMPLE_S16LE;
|
||||
params.prefs = CUBEB_STREAM_PREF_NONE;
|
||||
params.layout = CUBEB_LAYOUT_STEREO;
|
||||
|
||||
u32 latency{0};
|
||||
const auto latency_error = cubeb_get_min_latency(ctx, ¶ms, &latency);
|
||||
if (latency_error != CUBEB_OK) {
|
||||
LOG_ERROR(Audio_Sink, "Cubeb could not get min latency, it is not suitable.");
|
||||
return false;
|
||||
}
|
||||
latency = (std::max)(latency, TargetSampleCount * 2);
|
||||
|
||||
// Test opening a device with standard parameters
|
||||
cubeb_devid output_device{0};
|
||||
cubeb_devid input_device{0};
|
||||
std::string name{"Eden test"};
|
||||
cubeb_stream* stream{nullptr};
|
||||
|
||||
if (cubeb_stream_init(ctx, &stream, name.c_str(), input_device, nullptr, output_device, ¶ms,
|
||||
latency, &TmpDataCallback, &TmpStateCallback, nullptr) != CUBEB_OK) {
|
||||
LOG_CRITICAL(Audio_Sink, "Cubeb could not open a device, it is not suitable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
cubeb_stream_stop(stream);
|
||||
cubeb_stream_destroy(stream);
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
*/
|
||||
|
||||
} // namespace AudioCore::Sink
|
||||
@@ -0,0 +1,118 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <cubeb/cubeb.h>
|
||||
|
||||
#include "audio_core/sink/sink.h"
|
||||
|
||||
namespace Core {
|
||||
class System;
|
||||
}
|
||||
|
||||
namespace AudioCore::Sink {
|
||||
class SinkStream;
|
||||
|
||||
/**
|
||||
* Cubeb backend sink, holds multiple output streams and is responsible for sinking samples to
|
||||
* hardware. Used by Audio Render, Audio In and Audio Out.
|
||||
*/
|
||||
class CubebSink final : public Sink {
|
||||
public:
|
||||
explicit CubebSink(std::string_view device_id);
|
||||
~CubebSink() override;
|
||||
|
||||
/**
|
||||
* Create a new sink stream.
|
||||
*
|
||||
* @param system - Core system.
|
||||
* @param system_channels - Number of channels the audio system expects.
|
||||
* May differ from the device's channel count.
|
||||
* @param name - Name of this stream.
|
||||
* @param type - Type of this stream, render/in/out.
|
||||
*
|
||||
* @return A pointer to the created SinkStream
|
||||
*/
|
||||
SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels,
|
||||
const std::string& name, StreamType type) override;
|
||||
|
||||
/**
|
||||
* Close a given stream.
|
||||
*
|
||||
* @param stream - The stream to close.
|
||||
*/
|
||||
void CloseStream(SinkStream* stream) override;
|
||||
|
||||
/**
|
||||
* Close all streams.
|
||||
*/
|
||||
void CloseStreams() override;
|
||||
|
||||
/**
|
||||
* Get the device volume. Set from calls to the IAudioDevice service.
|
||||
*
|
||||
* @return Volume of the device.
|
||||
*/
|
||||
f32 GetDeviceVolume() const override;
|
||||
|
||||
/**
|
||||
* Set the device volume. Set from calls to the IAudioDevice service.
|
||||
*
|
||||
* @param volume - New volume of the device.
|
||||
*/
|
||||
void SetDeviceVolume(f32 volume) override;
|
||||
|
||||
/**
|
||||
* Set the system volume. Comes from the audio system using this stream.
|
||||
*
|
||||
* @param volume - New volume of the system.
|
||||
*/
|
||||
void SetSystemVolume(f32 volume) override;
|
||||
|
||||
private:
|
||||
/// Backend Cubeb context
|
||||
cubeb* ctx{};
|
||||
/// Cubeb id of the actual hardware output device
|
||||
cubeb_devid output_device{};
|
||||
/// Cubeb id of the actual hardware input device
|
||||
cubeb_devid input_device{};
|
||||
/// Vector of streams managed by this sink
|
||||
std::vector<SinkStreamPtr> sink_streams{};
|
||||
|
||||
#ifdef _WIN32
|
||||
/// Cubeb required COM to be initialized multi-threaded on Windows
|
||||
u32 com_init_result = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of connected devices from Cubeb.
|
||||
*
|
||||
* @param capture - Return input (capture) devices if true, otherwise output devices.
|
||||
*/
|
||||
std::vector<std::string> ListCubebSinkDevices(bool capture);
|
||||
|
||||
// REVERSION - function GetCubebLatency() reintroduced from EA-3833 - DIABLO 3 FIX
|
||||
/**
|
||||
* Get the reported latency for this sink.
|
||||
*
|
||||
* @return Minimum latency for this sink.
|
||||
*/
|
||||
u32 GetCubebLatency();
|
||||
|
||||
/**
|
||||
* Check if this backend is suitable for use.
|
||||
* Checks if enabled, its latency, whether it opens successfully, etc.
|
||||
*
|
||||
* @return True is this backend is suitable, false otherwise.
|
||||
*/
|
||||
// bool IsCubebSuitable(); // REVERTED BACK TO GetCubebLatency() FROM 3833
|
||||
|
||||
} // namespace AudioCore::Sink
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -38,6 +41,7 @@ public:
|
||||
StreamType type) override {
|
||||
if (null_sink == nullptr) {
|
||||
null_sink = std::make_unique<NullSinkStreamImpl>(system, type);
|
||||
null_sink->SetDeviceVolume(device_volume);
|
||||
}
|
||||
return null_sink.get();
|
||||
}
|
||||
@@ -45,9 +49,14 @@ public:
|
||||
void CloseStream(SinkStream*) override {}
|
||||
void CloseStreams() override {}
|
||||
f32 GetDeviceVolume() const override {
|
||||
return 1.0f;
|
||||
return device_volume;
|
||||
}
|
||||
void SetDeviceVolume(f32 volume) override {
|
||||
device_volume = volume;
|
||||
if (null_sink != nullptr) {
|
||||
null_sink->SetDeviceVolume(volume);
|
||||
}
|
||||
}
|
||||
void SetDeviceVolume(f32 volume) override {}
|
||||
void SetSystemVolume(f32 volume) override {}
|
||||
|
||||
private:
|
||||
|
||||
@@ -25,10 +25,9 @@ namespace {
|
||||
// See https://github.com/PCSX2/pcsx2/pull/12312
|
||||
// "SDL and cubeb backends previously resulted in different names for the output which
|
||||
// caused them be identified as different applications by the OS."
|
||||
//
|
||||
// Keep in sync with cubeb_sink.cpp name.
|
||||
SDL_SetHint("SDL_AUDIO_DEVICE_APP_NAME", "yuzu Latency Getter");
|
||||
SDL_SetHint("SDL_HINT_AUDIO_DEVICE_STREAM_ROLE", "Game");
|
||||
// We do our own processing, so just have SDL copy our audio
|
||||
SDL_SetHint("SDL_HINT_AUDIO_DEVICE_RAW_STREAM", "1");
|
||||
#ifdef __ANDROID__
|
||||
SDL_SetHintWithPriority(SDL_HINT_AUDIO_DRIVER, "openslES", SDL_HINT_OVERRIDE);
|
||||
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
|
||||
@@ -91,18 +90,23 @@ public:
|
||||
|
||||
SDL_AudioSpec spec{};
|
||||
spec.freq = TargetSampleRate;
|
||||
spec.channels = u8(device_channels);
|
||||
spec.format = SDL_AUDIO_S16LE;
|
||||
spec.channels = static_cast<u8>(device_channels);
|
||||
spec.format = SDL_AUDIO_S16;
|
||||
|
||||
auto const is_capture = (type == StreamType::In);
|
||||
std::string device_name = is_capture ? input_device : output_device;
|
||||
const SDL_AudioDeviceID audio_device = device_name.empty()
|
||||
? (is_capture
|
||||
? SDL_AUDIO_DEVICE_DEFAULT_RECORDING
|
||||
: SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK)
|
||||
: FindAudioDeviceByName(device_name, is_capture);
|
||||
std::string device_name{output_device};
|
||||
bool capture{false};
|
||||
if (type == StreamType::In) {
|
||||
device_name = input_device;
|
||||
capture = true;
|
||||
}
|
||||
|
||||
stream = SDL_OpenAudioDeviceStream(audio_device, &spec, &SDLSinkStream::DataCallback, this);
|
||||
const SDL_AudioDeviceID audio_device =
|
||||
device_name.empty() ? (capture ? SDL_AUDIO_DEVICE_DEFAULT_RECORDING
|
||||
: SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK)
|
||||
: FindAudioDeviceByName(device_name, capture);
|
||||
|
||||
stream = SDL_OpenAudioDeviceStream(audio_device, &spec, &SDLSinkStream::DataCallback,
|
||||
this);
|
||||
|
||||
if (stream == nullptr) {
|
||||
LOG_CRITICAL(Audio_Sink, "Error opening SDL audio device: {}", SDL_GetError());
|
||||
@@ -111,12 +115,13 @@ public:
|
||||
|
||||
SDL_AudioSpec stream_in{};
|
||||
SDL_AudioSpec stream_out{};
|
||||
void(SDL_GetAudioStreamFormat(stream, &stream_in, &stream_out));
|
||||
static_cast<void>(SDL_GetAudioStreamFormat(stream, &stream_in, &stream_out));
|
||||
|
||||
LOG_INFO(Service_Audio,
|
||||
"Opening SDL stream {} with: rate {} channels {} (system channels {}) "
|
||||
" format {}",
|
||||
static_cast<const void*>(stream), stream_out.freq, stream_out.channels,
|
||||
system_channels, int(stream_out.format));
|
||||
system_channels, static_cast<int>(stream_out.format));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,12 +136,14 @@ public:
|
||||
* Finalize the sink stream.
|
||||
*/
|
||||
void Finalize() override {
|
||||
if (stream != nullptr) {
|
||||
Stop();
|
||||
SDL_ClearAudioStream(stream);
|
||||
SDL_DestroyAudioStream(stream);
|
||||
stream = nullptr;
|
||||
if (stream == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
Stop();
|
||||
SDL_ClearAudioStream(stream);
|
||||
SDL_DestroyAudioStream(stream);
|
||||
stream = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,8 +156,9 @@ public:
|
||||
if (stream == nullptr || !paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
paused = false;
|
||||
void(SDL_ResumeAudioStreamDevice(stream));
|
||||
static_cast<void>(SDL_ResumeAudioStreamDevice(stream));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,7 +169,7 @@ public:
|
||||
return;
|
||||
}
|
||||
SignalPause();
|
||||
void(SDL_PauseAudioStreamDevice(stream));
|
||||
static_cast<void>(SDL_PauseAudioStreamDevice(stream));
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -173,34 +181,51 @@ private:
|
||||
* @param stream - Buffer of samples to be filled or read.
|
||||
* @param len - Length of the stream in bytes.
|
||||
*/
|
||||
static void DataCallback(void* userdata, SDL_AudioStream* stream, int additional_amount, int total_amount) {
|
||||
if (auto* impl = static_cast<SDLSinkStream*>(userdata); impl) {
|
||||
auto const num_channels = impl->GetDeviceChannels();
|
||||
auto const frame_size = num_channels;
|
||||
if (impl->type == StreamType::In) {
|
||||
auto const bytes_available = SDL_GetAudioStreamAvailable(stream);
|
||||
if (bytes_available > 0) {
|
||||
std::vector<s16> input(bytes_available / int(sizeof(s16)));
|
||||
auto const bytes_read = SDL_GetAudioStreamData(stream, input.data(), bytes_available);
|
||||
if (bytes_read > 0) {
|
||||
auto const num_frames = std::size_t(bytes_read) / sizeof(s16) / frame_size;
|
||||
std::span<const s16> input_buffer{input.data(), std::size_t(bytes_read) / sizeof(s16)};
|
||||
impl->ProcessAudioIn(input_buffer, num_frames);
|
||||
}
|
||||
}
|
||||
} else if (additional_amount > 0 || total_amount > 0) {
|
||||
auto const bytes_requested = additional_amount > 0 ? additional_amount : total_amount;
|
||||
std::vector<s16> output(bytes_requested / int(sizeof(s16)));
|
||||
auto const num_frames = std::size_t(bytes_requested) / sizeof(s16) / frame_size;
|
||||
std::span<s16> output_buffer{output.data(), output.size()};
|
||||
impl->ProcessAudioOutAndRender(output_buffer, num_frames);
|
||||
void(SDL_PutAudioStreamData(stream, output.data(), bytes_requested));
|
||||
static void DataCallback(void* userdata, SDL_AudioStream* stream, int additional_amount,
|
||||
int total_amount) {
|
||||
auto* impl = static_cast<SDLSinkStream*>(userdata);
|
||||
|
||||
if (!impl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t num_channels = impl->GetDeviceChannels();
|
||||
const std::size_t frame_size = num_channels;
|
||||
|
||||
if (impl->type == StreamType::In) {
|
||||
const int bytes_available = SDL_GetAudioStreamAvailable(stream);
|
||||
if (bytes_available <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<s16> input(bytes_available / static_cast<int>(sizeof(s16)));
|
||||
const int bytes_read = SDL_GetAudioStreamData(stream, input.data(), bytes_available);
|
||||
if (bytes_read <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t num_frames =
|
||||
static_cast<std::size_t>(bytes_read) / sizeof(s16) / frame_size;
|
||||
std::span<const s16> input_buffer{input.data(),
|
||||
static_cast<std::size_t>(bytes_read) / sizeof(s16)};
|
||||
impl->ProcessAudioIn(input_buffer, num_frames);
|
||||
} else {
|
||||
if (additional_amount <= 0 && total_amount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int bytes_requested = additional_amount > 0 ? additional_amount : total_amount;
|
||||
std::vector<s16> output(bytes_requested / static_cast<int>(sizeof(s16)));
|
||||
const std::size_t num_frames =
|
||||
static_cast<std::size_t>(bytes_requested) / sizeof(s16) / frame_size;
|
||||
std::span<s16> output_buffer{output.data(), output.size()};
|
||||
impl->ProcessAudioOutAndRender(output_buffer, num_frames);
|
||||
static_cast<void>(SDL_PutAudioStreamData(stream, output.data(), bytes_requested));
|
||||
}
|
||||
}
|
||||
|
||||
/// SDL stream attached to an opened input/output device
|
||||
SDL_AudioStream* stream = nullptr;
|
||||
SDL_AudioStream* stream{};
|
||||
};
|
||||
|
||||
SDLSink::SDLSink(std::string_view target_device_name) {
|
||||
@@ -216,9 +241,12 @@ SDLSink::SDLSink(std::string_view target_device_name) {
|
||||
|
||||
SDLSink::~SDLSink() = default;
|
||||
|
||||
SinkStream* SDLSink::AcquireSinkStream(Core::System& system, u32 system_channels_, const std::string&, StreamType type) {
|
||||
SinkStream* SDLSink::AcquireSinkStream(Core::System& system, u32 system_channels_,
|
||||
const std::string&, StreamType type) {
|
||||
system_channels = system_channels_;
|
||||
SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<SDLSinkStream>(device_channels, system_channels, output_device, input_device, type, system));
|
||||
SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<SDLSinkStream>(
|
||||
device_channels, system_channels, output_device, input_device, type, system));
|
||||
stream->SetDeviceVolume(device_volume);
|
||||
return stream.get();
|
||||
}
|
||||
|
||||
@@ -237,10 +265,11 @@ void SDLSink::CloseStreams() {
|
||||
}
|
||||
|
||||
f32 SDLSink::GetDeviceVolume() const {
|
||||
return sink_streams.empty() ? 1.0f : sink_streams[0]->GetDeviceVolume();
|
||||
return device_volume;
|
||||
}
|
||||
|
||||
void SDLSink::SetDeviceVolume(f32 volume) {
|
||||
device_volume = volume;
|
||||
for (auto& stream : sink_streams) {
|
||||
stream->SetDeviceVolume(volume);
|
||||
}
|
||||
@@ -252,21 +281,68 @@ void SDLSink::SetSystemVolume(f32 volume) {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ListSDLSinkDevices(bool is_capture) {
|
||||
std::vector<std::string> device_list{};
|
||||
if (InitializeAudio()) {
|
||||
int device_count = 0;
|
||||
SDL_AudioDeviceID* devices = is_capture ? SDL_GetAudioRecordingDevices(&device_count) : SDL_GetAudioPlaybackDevices(&device_count);
|
||||
if (devices != nullptr) {
|
||||
for (int i = 0; i < device_count; ++i) {
|
||||
if (const char* name = SDL_GetAudioDeviceName(devices[i]); name) {
|
||||
device_list.emplace_back(name);
|
||||
}
|
||||
}
|
||||
SDL_free(devices);
|
||||
std::vector<std::string> ListSDLSinkDevices(bool capture) {
|
||||
if (!InitializeAudio())
|
||||
return {}; //no devices
|
||||
|
||||
std::vector<std::string> device_list;
|
||||
int device_count = 0;
|
||||
SDL_AudioDeviceID* devices =
|
||||
capture ? SDL_GetAudioRecordingDevices(&device_count)
|
||||
: SDL_GetAudioPlaybackDevices(&device_count);
|
||||
if (devices == nullptr) {
|
||||
return device_list;
|
||||
}
|
||||
|
||||
for (int i = 0; i < device_count; ++i) {
|
||||
if (const char* name = SDL_GetAudioDeviceName(devices[i])) {
|
||||
device_list.emplace_back(name);
|
||||
}
|
||||
}
|
||||
SDL_free(devices);
|
||||
|
||||
return device_list;
|
||||
}
|
||||
|
||||
/* REVERSION to 3833 - function GetSDLLatency() REINTRODUCED FROM 3833 - DIABLO 3 FIX */
|
||||
u32 GetSDLLatency() {
|
||||
return TargetSampleCount * 2;
|
||||
}
|
||||
|
||||
// REVERTED back to 3833 - Below function IsSDLSuitable() removed, reverting to GetSDLLatency() above. - DIABLO 3 FIX
|
||||
/*
|
||||
bool IsSDLSuitable() {
|
||||
#if !defined(HAVE_SDL3)
|
||||
return false;
|
||||
#else
|
||||
// Check SDL can init
|
||||
if (!InitializeAudio()!
|
||||
return false;
|
||||
|
||||
// We can set any latency frequency we want with SDL, so no need to check that.
|
||||
|
||||
// Check we can open a device with standard parameters
|
||||
SDL_AudioSpec spec;
|
||||
spec.freq = TargetSampleRate;
|
||||
spec.channels = 2u;
|
||||
spec.format = AUDIO_S16SYS;
|
||||
spec.samples = TargetSampleCount * 2;
|
||||
spec.callback = nullptr;
|
||||
spec.userdata = nullptr;
|
||||
|
||||
SDL_AudioSpec obtained;
|
||||
auto device = SDL_OpenAudioDevice(nullptr, false, &spec, &obtained, false);
|
||||
|
||||
if (device == 0) {
|
||||
LOG_ERROR(Audio_Sink, "SDL failed to open a device, it is not suitable. Error: {}",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_CloseAudioDevice(device);
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
*/
|
||||
|
||||
} // namespace AudioCore::Sink
|
||||
|
||||
@@ -90,4 +90,20 @@ private:
|
||||
*/
|
||||
std::vector<std::string> ListSDLSinkDevices(bool capture);
|
||||
|
||||
// REVERSION - function GetSDLLatency() reintroduced from EA-3833 - DIABLO 3 FIX
|
||||
/**
|
||||
* Get the reported latency for this sink.
|
||||
*
|
||||
* @return Minimum latency for this sink.
|
||||
*/
|
||||
u32 GetSDLLatency();
|
||||
|
||||
/** REVERTED back to 3833 - Below function IsSDLSuitable() removed, reverting to GetSDLLatency() above. - DIABLO 3 FIX
|
||||
* Check if this backend is suitable for use.
|
||||
* Checks if enabled, its latency, whether it opens successfully, etc.
|
||||
*
|
||||
* @return True is this backend is suitable, false otherwise.
|
||||
*/
|
||||
//bool IsSDLSuitable(); // REVERTED for GetSDLLatency() from EA-3833
|
||||
|
||||
} // namespace AudioCore::Sink
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -96,6 +99,8 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
/// Master volume, persists stream lifetimes
|
||||
f32 device_volume{1.0f};
|
||||
/// Number of device channels supported by the hardware
|
||||
u32 device_channels{2};
|
||||
/// Number of channels the game is sending
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#include <vector>
|
||||
|
||||
#include "audio_core/sink/sink_details.h"
|
||||
#ifdef HAVE_CUBEB
|
||||
#include "audio_core/sink/cubeb_sink.h"
|
||||
#endif
|
||||
#ifdef HAVE_SDL3
|
||||
#include "audio_core/sink/sdl3_sink.h"
|
||||
#endif
|
||||
@@ -31,10 +34,24 @@ struct SinkDetails {
|
||||
FactoryFn factory;
|
||||
/// A method to call to list available devices.
|
||||
ListDevicesFn list_devices;
|
||||
/// Method to get the latency of this backend - REINTRODUCED FROM 3833 - DIABLO 3 FIX
|
||||
LatencyFn latency;
|
||||
/// Check whether this backend is suitable to be used.
|
||||
/// SuitableFn is_suitable; // REVERTED FOR LatencyFn latency ABOVE - DIABLO 3 FIX
|
||||
};
|
||||
|
||||
// sink_details is ordered in terms of desirability, with the best choice at the top.
|
||||
static constexpr SinkDetails sink_details[] = {
|
||||
constexpr SinkDetails sink_details[] = {
|
||||
#ifdef HAVE_CUBEB
|
||||
SinkDetails{
|
||||
Settings::AudioEngine::Cubeb,
|
||||
[](std::string_view device_id) -> std::unique_ptr<Sink> {
|
||||
return std::make_unique<CubebSink>(device_id);
|
||||
},
|
||||
&ListCubebSinkDevices,
|
||||
&GetCubebLatency,
|
||||
},
|
||||
#endif
|
||||
#ifdef HAVE_SDL3
|
||||
SinkDetails{
|
||||
Settings::AudioEngine::Sdl3,
|
||||
@@ -42,6 +59,7 @@ static constexpr SinkDetails sink_details[] = {
|
||||
return std::make_unique<SDLSink>(device_id);
|
||||
},
|
||||
&ListSDLSinkDevices,
|
||||
&GetSDLLatency,
|
||||
},
|
||||
#endif
|
||||
SinkDetails{
|
||||
@@ -50,32 +68,69 @@ static constexpr SinkDetails sink_details[] = {
|
||||
return std::make_unique<NullSink>(device_id);
|
||||
},
|
||||
[](bool capture) { return std::vector<std::string>{"null"}; },
|
||||
[]() { return 0u; },
|
||||
},
|
||||
};
|
||||
|
||||
const SinkDetails& GetOutputSinkDetails(Settings::AudioEngine sink_id) {
|
||||
const auto find_backend{[](Settings::AudioEngine id) {
|
||||
return std::ranges::find_if(std::begin(sink_details), std::end(sink_details), [&id](const auto& e) {
|
||||
return e.id == id;
|
||||
});
|
||||
return std::find_if(std::begin(sink_details), std::end(sink_details),
|
||||
[&id](const auto& sink_detail) { return sink_detail.id == id; });
|
||||
}};
|
||||
|
||||
auto iter = find_backend(sink_id);
|
||||
if (sink_id == Settings::AudioEngine::Cubeb || sink_id == Settings::AudioEngine::Auto) {
|
||||
|
||||
if (sink_id == Settings::AudioEngine::Auto) {
|
||||
// REVERTED TO 3833 BELOW - DIABLO 3 FIX
|
||||
/*
|
||||
// Auto-select a backend. Use the sink details ordering, preferring cubeb first, checking
|
||||
// that the backend is available and suitable to use.
|
||||
for (auto& details : sink_details) {
|
||||
if (details.is_suitable()) {
|
||||
iter = &details;
|
||||
break;
|
||||
}
|
||||
}
|
||||
*/ // END REVERTED CODE - DIABLO 3 FIX
|
||||
|
||||
// BEGIN REINTRODUCED FROM 3833 - REPLACED CODE BLOCK ABOVE - DIABLO 3 FIX
|
||||
// Auto-select a backend. Prefer CubeB, but it may report a large minimum latency which
|
||||
// causes audio issues, in that case go with SDL.
|
||||
#if defined(HAVE_CUBEB) && defined(HAVE_SDL3)
|
||||
iter = find_backend(Settings::AudioEngine::Cubeb);
|
||||
if (iter->latency() > TargetSampleCount * 3) {
|
||||
iter = find_backend(Settings::AudioEngine::Sdl3);
|
||||
LOG_INFO(Service_Audio, "Auto-selecting the {} backend", Settings::CanonicalizeEnum(iter->id));
|
||||
}
|
||||
#else
|
||||
iter = std::begin(sink_details);
|
||||
#endif
|
||||
// END REINTRODUCED SECTION FROM 3833 - DIABLO 3 FIX
|
||||
LOG_INFO(Service_Audio, "Auto-selecting the {} backend",
|
||||
Settings::CanonicalizeEnum(iter->id));
|
||||
/* BEGIN REMOVED - REVERTING BACK TO 3833, this didn't exist at all. - DIABLO 3 FIX
|
||||
} else {
|
||||
if (iter != std::end(sink_details) && !iter->is_suitable()) {
|
||||
LOG_ERROR(Service_Audio, "Selected backend {} is not suitable, falling back to null",
|
||||
Settings::CanonicalizeEnum(iter->id));
|
||||
iter = find_backend(Settings::AudioEngine::Null);
|
||||
} */ // END REMOVED REVERT - DIABLO 3 FIX
|
||||
}
|
||||
|
||||
if (iter == std::end(sink_details)) {
|
||||
LOG_ERROR(Audio, "Invalid sink_id {}", Settings::CanonicalizeEnum(sink_id));
|
||||
iter = find_backend(Settings::AudioEngine::Null);
|
||||
}
|
||||
|
||||
return *iter;
|
||||
}
|
||||
} // Anonymous namespace
|
||||
|
||||
std::vector<Settings::AudioEngine> GetSinkIDs() {
|
||||
std::vector<Settings::AudioEngine> sink_ids(std::size(sink_details));
|
||||
std::transform(std::begin(sink_details), std::end(sink_details), std::begin(sink_ids), [](const auto& sink) { return sink.id; });
|
||||
|
||||
std::transform(std::begin(sink_details), std::end(sink_details), std::begin(sink_ids),
|
||||
[](const auto& sink) { return sink.id; });
|
||||
|
||||
return sink_ids;
|
||||
}
|
||||
|
||||
|
||||
@@ -459,7 +459,7 @@ struct Values {
|
||||
&frame_gen};
|
||||
|
||||
SwitchableSetting<u32, true> frame_gen_queue_target{linkage,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
"frame_gen_queue_target",
|
||||
@@ -469,9 +469,6 @@ struct Values {
|
||||
false,
|
||||
&frame_gen};
|
||||
|
||||
SwitchableSetting<bool> frame_gen_fp16{linkage, true, "frame_gen_fp16", Category::Renderer,
|
||||
Specialization::Default, true, false, &frame_gen};
|
||||
|
||||
SwitchableSetting<bool> frame_gen_dump_flow{linkage, false, "frame_gen_dump_flow",
|
||||
Category::Renderer};
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ add_library(core STATIC
|
||||
core_timing.h
|
||||
cpu_manager.cpp
|
||||
cpu_manager.h
|
||||
launch_params.cpp
|
||||
launch_params.h
|
||||
crypto/aes_util.cpp
|
||||
crypto/aes_util.h
|
||||
crypto/ctr_encryption_layer.cpp
|
||||
@@ -1201,6 +1203,9 @@ endif()
|
||||
|
||||
target_include_directories(core PRIVATE ${OPUS_INCLUDE_DIRS})
|
||||
target_link_libraries(core PUBLIC common PRIVATE audio_core hid_core network video_core nx_tzdb tz)
|
||||
if (MSVC)
|
||||
target_link_libraries(core PRIVATE getopt)
|
||||
endif()
|
||||
|
||||
if (BOOST_NO_HEADERS)
|
||||
target_link_libraries(core PUBLIC Boost::container Boost::heap Boost::asio Boost::process Boost::crc)
|
||||
|
||||
@@ -73,16 +73,20 @@ Result DisplayLayerManager::CreateManagedDisplayLayer(u64* out_layer_id) {
|
||||
R_TRY(m_manager_display_service->CreateManagedLayer(
|
||||
out_layer_id, 0, display_id, Service::AppletResourceUserId{m_process->GetProcessId()}));
|
||||
|
||||
m_manager_display_service->SetLayerVisibility(m_visible, *out_layer_id);
|
||||
(void)m_display_service->GetContainer()->SetLayerStackMask(*out_layer_id,
|
||||
this->GetLayerStackMask());
|
||||
|
||||
if (m_applet_id != AppletId::Application) {
|
||||
(void)m_manager_display_service->SetLayerBlending(m_blending_enabled, *out_layer_id);
|
||||
if (m_applet_id == AppletId::OverlayDisplay) {
|
||||
(void)m_manager_display_service->SetLayerZIndex(-1, *out_layer_id);
|
||||
(void)m_display_service->GetContainer()->SetLayerIsOverlay(*out_layer_id, true);
|
||||
(void)m_manager_display_service->SetLayerZIndex(Overlay, *out_layer_id);
|
||||
} else {
|
||||
(void)m_manager_display_service->SetLayerZIndex(1, *out_layer_id);
|
||||
(void)m_manager_display_service->SetLayerZIndex(Foreground, *out_layer_id);
|
||||
}
|
||||
}
|
||||
(void)m_display_service->GetContainer()->SetLayerZIndex(*out_layer_id, true);
|
||||
|
||||
m_display_service->GetContainer()->SetLayerZIndex(*out_layer_id, true);
|
||||
m_managed_display_layers.emplace(*out_layer_id);
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -122,14 +126,16 @@ Result DisplayLayerManager::IsSystemBufferSharingEnabled() {
|
||||
|
||||
// Ensure the overlay layer is visible
|
||||
m_manager_display_service->SetLayerVisibility(m_visible, m_system_shared_layer_id);
|
||||
m_manager_display_service->SetLayerBlending(m_blending_enabled, m_system_shared_layer_id);
|
||||
s32 initial_z = 1;
|
||||
(void)m_display_service->GetContainer()->SetLayerZIndex(m_system_shared_layer_id, true);
|
||||
(void)m_manager_display_service->SetLayerBlending(m_blending_enabled, m_system_shared_layer_id);
|
||||
s32 initial_z = Foreground;
|
||||
if (m_applet_id == AppletId::OverlayDisplay) {
|
||||
initial_z = -1;
|
||||
initial_z = Overlay;
|
||||
(void)m_manager_display_service->SetLayerZIndex(initial_z, m_system_shared_layer_id);
|
||||
(void)m_display_service->GetContainer()->SetLayerIsOverlay(m_system_shared_layer_id, true);
|
||||
}
|
||||
m_manager_display_service->SetLayerZIndex(initial_z, m_system_shared_layer_id);
|
||||
m_display_service->GetContainer()->SetLayerZIndex(m_system_shared_layer_id, true);
|
||||
m_managed_display_layers.emplace(m_system_shared_layer_id);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ IReadOnlyApplicationControlDataInterface::IReadOnlyApplicationControlDataInterfa
|
||||
{10, &IReadOnlyApplicationControlDataInterface::ListApplicationIcon, "ListApplicationIcon"},
|
||||
{13, &IReadOnlyApplicationControlDataInterface::ListApplicationTitle, "ListApplicationTitle"},
|
||||
{19, D<&IReadOnlyApplicationControlDataInterface::GetApplicationControlData3>, "GetApplicationControlData"},
|
||||
{23, D<&IReadOnlyApplicationControlDataInterface::GetApplicationControlData3>, "GetApplicationControlData"},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ protected:
|
||||
/// @param expected_header_ request header in the command buffer which will trigger dispatch to this handler
|
||||
/// @param handler_callback_ member function in this service which will be called to handle the request
|
||||
/// @param name_ human-friendly name for the request. Used mostly for logging purposes.
|
||||
constexpr FunctionInfoTyped(u32 expected_header_, HandlerFnP<T> handler_callback_, const char* name_)
|
||||
FunctionInfoTyped(u32 expected_header_, HandlerFnP<T> handler_callback_, const char* name_)
|
||||
: FunctionInfoBase{expected_header_, HandlerFnP<ServiceFrameworkBase>(handler_callback_), name_} {}
|
||||
};
|
||||
using FunctionInfo = FunctionInfoTyped<Self>;
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <regex>
|
||||
#include <cctype>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/scm_rev.h"
|
||||
#include "core/core.h"
|
||||
#include "core/hle/service/acc/profile_manager.h"
|
||||
#include "core/launch_params.h"
|
||||
|
||||
#undef _UNICODE
|
||||
#include <getopt.h>
|
||||
#ifndef _MSC_VER
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Core {
|
||||
LaunchParams ParseLaunchParams(Core::System& system, int argc, char *argv[], wchar_t *argv_w[]) noexcept {
|
||||
LaunchParams p{};
|
||||
|
||||
int option_index = 0;
|
||||
static struct option long_options[] = {
|
||||
// clang-format off
|
||||
{"debug", no_argument, 0, 'd'},
|
||||
{"config", required_argument, 0, 'c'},
|
||||
{"fullscreen", no_argument, 0, 'f'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{"game", required_argument, 0, 'g'},
|
||||
{"multiplayer", required_argument, 0, 'm'},
|
||||
{"program", optional_argument, 0, 'p'},
|
||||
{"user", required_argument, 0, 'u'},
|
||||
{"version", no_argument, 0, 'v'},
|
||||
{"input-profile", no_argument, 0, 'i'},
|
||||
{"null-render", no_argument, 0, 'n'},
|
||||
{"singlecore", no_argument, 0, 's'},
|
||||
{"filter", no_argument, 0, 'x'},
|
||||
{0, 0, 0, 0},
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
while (optind < argc) {
|
||||
if (int arg = getopt_long(argc, argv, "dc:fhg:m:p:u:vinsx", long_options, &option_index); arg != -1) {
|
||||
switch (char(arg)) {
|
||||
case 'd':
|
||||
p.override_gdb_port = uint16_t(atoi(optarg));
|
||||
break;
|
||||
case 'c':
|
||||
p.config_path = optarg;
|
||||
break;
|
||||
case 'f':
|
||||
p.fullscreen = true;
|
||||
LOG_INFO(Frontend, "Starting in fullscreen mode...");
|
||||
break;
|
||||
case 'h':
|
||||
p.print_help = true;
|
||||
break;
|
||||
case 'g':
|
||||
p.filepath = std::string(optarg);
|
||||
break;
|
||||
case 'i': {
|
||||
p.input_profile = std::string(optarg);
|
||||
break;
|
||||
}
|
||||
case 'm': {
|
||||
p.use_multiplayer = true;
|
||||
const std::string str_arg(optarg);
|
||||
// regex to check if the format is nickname:password@ip:port
|
||||
// with optional :password
|
||||
const std::regex re("^([^:]+)(?::(.+))?@([^:]+)(?::([0-9]+))?$");
|
||||
if (std::regex_match(str_arg, re)) {
|
||||
std::smatch match;
|
||||
std::regex_search(str_arg, match, re);
|
||||
ASSERT(match.size() == 5);
|
||||
p.nickname = match[1];
|
||||
p.password = match[2];
|
||||
p.address = match[3];
|
||||
if (!match[4].str().empty()) {
|
||||
p.port = u16(std::strtoul(match[4].str().c_str(), nullptr, 0));
|
||||
}
|
||||
std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$");
|
||||
ASSERT(std::regex_match(p.nickname, nickname_re) && "Nickname is not valid. Must be 4 to 20 alphanumeric characters");
|
||||
ASSERT(!p.address.empty() && "Address is empty");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'p':
|
||||
p.program_args = argv[optind];
|
||||
++optind;
|
||||
break;
|
||||
case 'u': {
|
||||
// Launch game with a specific user
|
||||
bool argument_ok = isdigit(optarg[0]);
|
||||
p.selected_user = atoi(optarg);
|
||||
if (!argument_ok) {
|
||||
// try to look it up by username, only finds the first username that matches.
|
||||
auto const user_idx = system.GetProfileManager().GetUserIndex(optarg);
|
||||
if (user_idx != std::nullopt) {
|
||||
p.selected_user = user_idx.value();
|
||||
} else {
|
||||
LOG_ERROR(Frontend, "Invalid user argument '{}'", optarg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (system.GetProfileManager().UserExistsIndex(*p.selected_user)) {
|
||||
Settings::values.current_user = s32(*p.selected_user);
|
||||
} else {
|
||||
LOG_ERROR(Frontend, "Selected user {} doesn't exist", *p.selected_user);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'v':
|
||||
p.print_version = true;
|
||||
break;
|
||||
case 'n':
|
||||
p.force_null_render = true;
|
||||
break;
|
||||
case 's':
|
||||
p.force_single_core = true;
|
||||
break;
|
||||
case 'x':
|
||||
p.log_filter = argv[optind];
|
||||
++optind;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// only kept due to shortcuts made by Qt frontend which use "-qlaunch"
|
||||
if (strcmp(argv[optind], "-hlaunch") == 0) {
|
||||
p.launch_hlaunch = true;
|
||||
} else if (strcmp(argv[optind], "-qlaunch") == 0) {
|
||||
p.launch_qlaunch = true;
|
||||
} else if (strcmp(argv[optind], "-setup") == 0) {
|
||||
p.launch_setup = true;
|
||||
} else {
|
||||
// qt feeds utf8 data, sdl frontend feeds raw utf16 data
|
||||
#ifdef _WIN32
|
||||
p.filepath = argv_w != nullptr
|
||||
? Common::UTF16ToUTF8(argv_w[optind])
|
||||
: argv[optind];
|
||||
#else
|
||||
p.filepath = argv[optind];
|
||||
#endif
|
||||
}
|
||||
optind++;
|
||||
}
|
||||
}
|
||||
p.argv0 = argv[0];
|
||||
return p;
|
||||
}
|
||||
|
||||
void ApplyLaunchParams(LaunchParams const& lp) noexcept {
|
||||
// apply the log_filter setting
|
||||
// the logger was initialized before and doesn't pick up the filter on its own
|
||||
Common::Log::Filter filter{};
|
||||
filter.ParseFilterString(lp.log_filter.value_or(Settings::values.log_filter.GetValue()));
|
||||
Common::Log::SetGlobalFilter(filter);
|
||||
|
||||
if (!lp.program_args.empty()) {
|
||||
Settings::values.program_args = lp.program_args;
|
||||
}
|
||||
if (!lp.input_profile.empty()) {
|
||||
auto& players = Settings::values.players.GetValue();
|
||||
players[0].profile_name = lp.input_profile;
|
||||
}
|
||||
if (lp.selected_user.has_value()) {
|
||||
Settings::values.current_user = std::clamp(*lp.selected_user, 0, 7);
|
||||
}
|
||||
if (lp.override_gdb_port.has_value()) {
|
||||
Settings::values.use_gdbstub = true;
|
||||
Settings::values.gdbstub_port = *lp.override_gdb_port;
|
||||
}
|
||||
if (lp.force_single_core) {
|
||||
Settings::values.use_multi_core = false;
|
||||
}
|
||||
if (lp.force_null_render) {
|
||||
Settings::values.renderer_backend = Settings::RendererBackend::Null;
|
||||
}
|
||||
|
||||
if (lp.print_version) {
|
||||
LOG_INFO(Frontend, "Eden {} {}", Common::g_scm_branch, Common::g_scm_desc);
|
||||
}
|
||||
if (lp.print_help) {
|
||||
LOG_INFO(Frontend,
|
||||
"Usage: {}"
|
||||
" [options] <filename>\n"
|
||||
"-c, --config Load the specified configuration file\n"
|
||||
"-f, --fullscreen Start in fullscreen mode\n"
|
||||
"-g, --game File path of the game to load\n"
|
||||
"-h, --help Display this help and exit\n"
|
||||
"-m, --multiplayer=nick:password@address:port"
|
||||
" Nickname, password, address and port for multiplayer\n"
|
||||
"-p, --program Pass following string as arguments to executable\n"
|
||||
"-u, --user Select a specific user profile from 0 to 7\n"
|
||||
"-d, --debug Run the GDB stub on a port from 1 to 65535\n"
|
||||
"-v, --version Output version information and exit\n",
|
||||
lp.argv0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include "common/common_types.h"
|
||||
#include "network/room.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
class System;
|
||||
struct LaunchParams {
|
||||
std::string argv0{};
|
||||
std::string filepath{};
|
||||
std::string nickname{};
|
||||
std::string password{};
|
||||
std::string address{};
|
||||
std::string input_profile{};
|
||||
std::string program_args{};
|
||||
std::optional<std::string> config_path{};
|
||||
std::optional<int> selected_user{};
|
||||
std::optional<u16> override_gdb_port{};
|
||||
std::optional<std::string> log_filter{};
|
||||
u16 port = Network::DefaultRoomPort;
|
||||
bool use_multiplayer = false;
|
||||
bool fullscreen = false;
|
||||
bool force_null_render = false;
|
||||
bool force_single_core = false;
|
||||
// print
|
||||
bool print_version = false;
|
||||
bool print_help = false;
|
||||
// qt
|
||||
bool launch_hlaunch = false;
|
||||
bool launch_qlaunch = false;
|
||||
bool launch_setup = false;
|
||||
};
|
||||
|
||||
LaunchParams ParseLaunchParams(Core::System& system, int argc, char *argv[], wchar_t *argv_w[]) noexcept;
|
||||
void ApplyLaunchParams(LaunchParams const& lp) noexcept;
|
||||
|
||||
}
|
||||
@@ -274,8 +274,8 @@ std::pair<oaknut::XReg, oaknut::XReg> InlinePageTableEmitVAddrLookup(oaknut::Cod
|
||||
code.LDR(Xscratch0, Xpagetable, Xscratch0);
|
||||
|
||||
if (ctx.conf.page_table_marked_bit) {
|
||||
// check for marked bit
|
||||
code.TBNZ(Xscratch0, *ctx.conf.page_table_marked_bit, *fallback);
|
||||
code.TST(Xscratch0, 1ULL << *ctx.conf.page_table_marked_bit);
|
||||
code.B(NE, *fallback);
|
||||
}
|
||||
|
||||
if (ctx.conf.page_table_pointer_mask != 0) {
|
||||
|
||||
@@ -4,17 +4,16 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <fmt/ranges.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "common/param_package.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/thread.h"
|
||||
#include "common/steady_clock.h"
|
||||
#include "input_common/drivers/mouse.h"
|
||||
|
||||
namespace InputCommon {
|
||||
constexpr int update_time = 10;
|
||||
constexpr float default_panning_sensitivity = 0.0010f;
|
||||
constexpr float default_stick_sensitivity = 0.0006f;
|
||||
constexpr float default_deadzone_counterweight = 0.01f;
|
||||
@@ -74,33 +73,31 @@ Mouse::Mouse(std::string input_engine_) : InputEngine(std::move(input_engine_))
|
||||
last_motion_change = {};
|
||||
}
|
||||
|
||||
void Mouse::UpdateStickInput() {
|
||||
if (!IsMousePanningEnabled()) {
|
||||
return;
|
||||
void Mouse::UpdateStickInput(Common::SteadyClock::time_point timestamp) {
|
||||
if (IsMousePanningEnabled()) {
|
||||
const float length = last_mouse_change.Length();
|
||||
|
||||
// Prevent input from exceeding the max range (1.0f) too much,
|
||||
// but allow some room to make it easier to sustain
|
||||
if (length > maximum_stick_range) {
|
||||
last_mouse_change /= length;
|
||||
last_mouse_change *= maximum_stick_range;
|
||||
}
|
||||
|
||||
SetAxis(identifier, mouse_axis_x, last_mouse_change[0]);
|
||||
SetAxis(identifier, mouse_axis_y, -last_mouse_change[1]);
|
||||
|
||||
// Decay input over time
|
||||
const float clamped_length = (std::min)(1.0f, length);
|
||||
const float decay_strength = Settings::values.mouse_panning_decay_strength.GetValue();
|
||||
const float decay = 1 - clamped_length * clamped_length * decay_strength * 0.01f;
|
||||
const float min_decay = Settings::values.mouse_panning_min_decay.GetValue();
|
||||
const float clamped_decay = (std::min)(1 - min_decay / 100.0f, decay);
|
||||
last_mouse_change *= clamped_decay;
|
||||
}
|
||||
|
||||
const float length = last_mouse_change.Length();
|
||||
|
||||
// Prevent input from exceeding the max range (1.0f) too much,
|
||||
// but allow some room to make it easier to sustain
|
||||
if (length > maximum_stick_range) {
|
||||
last_mouse_change /= length;
|
||||
last_mouse_change *= maximum_stick_range;
|
||||
}
|
||||
|
||||
SetAxis(identifier, mouse_axis_x, last_mouse_change[0]);
|
||||
SetAxis(identifier, mouse_axis_y, -last_mouse_change[1]);
|
||||
|
||||
// Decay input over time
|
||||
const float clamped_length = (std::min)(1.0f, length);
|
||||
const float decay_strength = Settings::values.mouse_panning_decay_strength.GetValue();
|
||||
const float decay = 1 - clamped_length * clamped_length * decay_strength * 0.01f;
|
||||
const float min_decay = Settings::values.mouse_panning_min_decay.GetValue();
|
||||
const float clamped_decay = (std::min)(1 - min_decay / 100.0f, decay);
|
||||
last_mouse_change *= clamped_decay;
|
||||
}
|
||||
|
||||
void Mouse::UpdateMotionInput() {
|
||||
void Mouse::UpdateMotionInput(Common::SteadyClock::time_point timestamp) {
|
||||
const float sensitivity =
|
||||
IsMousePanningEnabled() ? default_motion_panning_sensitivity : default_motion_sensitivity;
|
||||
|
||||
@@ -114,23 +111,21 @@ void Mouse::UpdateMotionInput() {
|
||||
last_motion_change[1] = last_motion_change[1] * multiplier;
|
||||
}
|
||||
|
||||
const BasicMotion motion_data{
|
||||
.gyro_x = last_motion_change[0] * sensitivity,
|
||||
.gyro_y = last_motion_change[1] * sensitivity,
|
||||
.gyro_z = last_motion_change[2] * sensitivity,
|
||||
.accel_x = 0,
|
||||
.accel_y = 0,
|
||||
.accel_z = 0,
|
||||
.delta_timestamp = update_time * 1000,
|
||||
};
|
||||
|
||||
if (IsMousePanningEnabled()) {
|
||||
last_motion_change[0] = 0;
|
||||
last_motion_change[1] = 0;
|
||||
}
|
||||
last_motion_change[2] = 0;
|
||||
|
||||
SetMotion(motion_identifier, 0, motion_data);
|
||||
SetMotion(motion_identifier, 0, BasicMotion{
|
||||
.gyro_x = last_motion_change[0] * sensitivity,
|
||||
.gyro_y = last_motion_change[1] * sensitivity,
|
||||
.gyro_z = last_motion_change[2] * sensitivity,
|
||||
.accel_x = 0,
|
||||
.accel_y = 0,
|
||||
.accel_z = 0,
|
||||
.delta_timestamp = u64(std::chrono::duration_cast<std::chrono::microseconds>(timestamp - last_notify_timestamp).count()),
|
||||
});
|
||||
}
|
||||
|
||||
void Mouse::Move(int x, int y, int center_x, int center_y) {
|
||||
@@ -149,29 +144,27 @@ void Mouse::Move(int x, int y, int center_x, int center_y) {
|
||||
last_mouse_change /= length;
|
||||
last_mouse_change *= deadzone_cw;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (button_pressed) {
|
||||
const auto mouse_move = Common::Vec<int, 2>(x, y) - mouse_origin;
|
||||
const float x_sensitivity =
|
||||
Settings::values.mouse_panning_x_sensitivity.GetValue() * default_stick_sensitivity;
|
||||
const float y_sensitivity =
|
||||
Settings::values.mouse_panning_y_sensitivity.GetValue() * default_stick_sensitivity;
|
||||
SetAxis(identifier, mouse_axis_x, float(mouse_move[0]) * x_sensitivity);
|
||||
SetAxis(identifier, mouse_axis_y, float(-mouse_move[1]) * y_sensitivity);
|
||||
|
||||
last_motion_change = {
|
||||
float(-mouse_move[1]) * x_sensitivity,
|
||||
float(-mouse_move[0]) * y_sensitivity,
|
||||
last_motion_change[2],
|
||||
};
|
||||
} else {
|
||||
if (button_pressed) {
|
||||
const auto mouse_move = Common::Vec<int, 2>(x, y) - mouse_origin;
|
||||
const float x_sensitivity = Settings::values.mouse_panning_x_sensitivity.GetValue() * default_stick_sensitivity;
|
||||
const float y_sensitivity = Settings::values.mouse_panning_y_sensitivity.GetValue() * default_stick_sensitivity;
|
||||
SetAxis(identifier, mouse_axis_x, float(mouse_move[0]) * x_sensitivity);
|
||||
SetAxis(identifier, mouse_axis_y, float(-mouse_move[1]) * y_sensitivity);
|
||||
last_motion_change = {
|
||||
float(-mouse_move[1]) * x_sensitivity,
|
||||
float(-mouse_move[0]) * y_sensitivity,
|
||||
last_motion_change[2],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Mouse::NotifyChanged() {
|
||||
UpdateStickInput();
|
||||
UpdateMotionInput();
|
||||
auto const timestamp = Common::SteadyClock::Now();
|
||||
UpdateStickInput(timestamp);
|
||||
UpdateMotionInput(timestamp);
|
||||
last_notify_timestamp = timestamp;
|
||||
}
|
||||
|
||||
void Mouse::MouseMove(f32 touch_x, f32 touch_y) {
|
||||
@@ -223,8 +216,8 @@ void Mouse::MouseWheelChange(int x, int y) {
|
||||
wheel_position[0] += x;
|
||||
wheel_position[1] += y;
|
||||
last_motion_change[2] += static_cast<f32>(y);
|
||||
SetAxis(identifier, wheel_axis_x, static_cast<f32>(wheel_position[0]));
|
||||
SetAxis(identifier, wheel_axis_y, static_cast<f32>(wheel_position[1]));
|
||||
SetAxis(identifier, wheel_axis_x, f32(wheel_position[0]));
|
||||
SetAxis(identifier, wheel_axis_y, f32(wheel_position[1]));
|
||||
}
|
||||
|
||||
void Mouse::ReleaseAllButtons() {
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
#include "common/steady_clock.h"
|
||||
#include "common/polyfill_thread.h"
|
||||
#include "common/vector_math.h"
|
||||
#include "input_common/input_engine.h"
|
||||
@@ -101,8 +103,8 @@ public:
|
||||
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
|
||||
|
||||
private:
|
||||
void UpdateStickInput();
|
||||
void UpdateMotionInput();
|
||||
void UpdateStickInput(Common::SteadyClock::time_point timestamp);
|
||||
void UpdateMotionInput(Common::SteadyClock::time_point timestamp);
|
||||
bool IsMousePanningEnabled();
|
||||
|
||||
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
|
||||
@@ -112,6 +114,7 @@ private:
|
||||
Common::Vec<float, 2> last_mouse_change;
|
||||
Common::Vec<float, 3> last_motion_change;
|
||||
Common::Vec<int, 2> wheel_position;
|
||||
Common::SteadyClock::time_point last_notify_timestamp{};
|
||||
bool button_pressed = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,3 @@ endif()
|
||||
if (SOLARIS)
|
||||
target_link_libraries(network PRIVATE socket nsl)
|
||||
endif()
|
||||
if (MINGW)
|
||||
target_link_libraries(network PRIVATE winmm)
|
||||
endif()
|
||||
|
||||
@@ -78,7 +78,7 @@ public:
|
||||
protected:
|
||||
static constexpr size_t UNSET_CHANNEL{(std::numeric_limits<size_t>::max)()};
|
||||
|
||||
P* channel_state;
|
||||
P* channel_state = nullptr;
|
||||
size_t current_channel_id{UNSET_CHANNEL};
|
||||
size_t current_address_space{};
|
||||
Tegra::Engines::Maxwell3D* maxwell3d{};
|
||||
|
||||
@@ -45,7 +45,7 @@ constexpr u32 PERFORMANCE_SHADER_ID_FIRST = 280;
|
||||
constexpr u32 PERFORMANCE_SHADER_ID_LAST = 302;
|
||||
|
||||
constexpr u32 CACHE_MAGIC = 0x4746534C;
|
||||
constexpr u32 CACHE_VERSION = 2;
|
||||
constexpr u32 CACHE_VERSION = 3;
|
||||
|
||||
struct CacheHeader {
|
||||
u32 magic;
|
||||
@@ -53,7 +53,6 @@ struct CacheHeader {
|
||||
u64 source_size;
|
||||
u64 source_hash;
|
||||
u32 module_count;
|
||||
u32 variant;
|
||||
};
|
||||
|
||||
struct Section {
|
||||
@@ -277,41 +276,19 @@ template <typename Map>
|
||||
return std::ranges::all_of(ids, [&](u32 id) { return resources.contains(id); });
|
||||
}
|
||||
|
||||
[[nodiscard]] u32 VariantOffset(ShaderVariant variant) {
|
||||
return variant == ShaderVariant::NativeFp16 ? PerformanceShader::NATIVE_FP16_OFFSET
|
||||
: PerformanceShader::NATIVE_FP32_OFFSET;
|
||||
}
|
||||
|
||||
template <typename Map>
|
||||
[[nodiscard]] bool HasNativeVariant(const Map& resources, ShaderVariant variant) {
|
||||
const u32 offset = VariantOffset(variant);
|
||||
[[nodiscard]] bool HasNativeShaders(const Map& resources) {
|
||||
return std::ranges::all_of(PerformanceShaderIds(), [&](u32 id) {
|
||||
const auto hit = resources.find(id + offset);
|
||||
const auto hit = resources.find(id + PerformanceShader::NATIVE_FP16_OFFSET);
|
||||
return hit != resources.end() && IsSpirvModule(hit->second);
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<ShaderVariant> SelectVariant(const ResourceSpans& resources,
|
||||
bool allow_fp16, bool prefer_fp16) {
|
||||
if (prefer_fp16 && HasNativeVariant(resources, ShaderVariant::NativeFp16)) {
|
||||
return ShaderVariant::NativeFp16;
|
||||
}
|
||||
if (HasNativeVariant(resources, ShaderVariant::NativeFp32)) {
|
||||
return ShaderVariant::NativeFp32;
|
||||
}
|
||||
if (allow_fp16 && HasNativeVariant(resources, ShaderVariant::NativeFp16)) {
|
||||
return ShaderVariant::NativeFp16;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]] LosslessStatus TranslateAll(const ResourceSpans& resources,
|
||||
ShaderModules& out_modules,
|
||||
ShaderVariant variant) {
|
||||
const u32 offset = VariantOffset(variant);
|
||||
ShaderModules& out_modules) {
|
||||
out_modules.clear();
|
||||
for (const u32 id : PerformanceShaderIds()) {
|
||||
const auto hit = resources.find(id + offset);
|
||||
const auto hit = resources.find(id + PerformanceShader::NATIVE_FP16_OFFSET);
|
||||
if (hit == resources.end()) {
|
||||
return LosslessStatus::MissingShaders;
|
||||
}
|
||||
@@ -343,7 +320,7 @@ template <typename Map>
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ReadShaderCache(const std::filesystem::path& path, u64 source_size,
|
||||
u64 source_hash, u32 variant, ShaderModules& out_modules) {
|
||||
u64 source_hash, ShaderModules& out_modules) {
|
||||
if (!Common::FS::Exists(path)) {
|
||||
return false;
|
||||
}
|
||||
@@ -355,8 +332,7 @@ template <typename Map>
|
||||
return false;
|
||||
}
|
||||
if (header.magic != CACHE_MAGIC || header.version != CACHE_VERSION ||
|
||||
header.source_size != source_size || header.source_hash != source_hash ||
|
||||
header.variant != variant) {
|
||||
header.source_size != source_size || header.source_hash != source_hash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -433,8 +409,10 @@ template <typename Map>
|
||||
return LosslessStatus::MissingShaders;
|
||||
}
|
||||
|
||||
return HasPerformanceShaders(out_resources) ? LosslessStatus::Ok
|
||||
: LosslessStatus::MissingShaders;
|
||||
if (!HasNativeShaders(out_resources)) {
|
||||
return LosslessStatus::MissingShaders;
|
||||
}
|
||||
return LosslessStatus::Ok;
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
@@ -483,7 +461,7 @@ LosslessStatus GetInstalledLosslessStatus() {
|
||||
return ValidateLosslessDll(GetLosslessDllPath());
|
||||
}
|
||||
|
||||
LosslessStatus LoadShaderModules(ShaderModules& out_modules, bool allow_fp16, bool prefer_fp16) {
|
||||
LosslessStatus LoadShaderModules(ShaderModules& out_modules) {
|
||||
std::vector<u8> image;
|
||||
const LosslessStatus read_status = ReadImageFile(GetLosslessDllPath(), image);
|
||||
if (read_status != LosslessStatus::Ok) {
|
||||
@@ -501,17 +479,11 @@ LosslessStatus LoadShaderModules(ShaderModules& out_modules, bool allow_fp16, bo
|
||||
return parse_status;
|
||||
}
|
||||
|
||||
const std::optional<ShaderVariant> variant = SelectVariant(spans, allow_fp16, prefer_fp16);
|
||||
if (!variant) {
|
||||
return LosslessStatus::MissingShaders;
|
||||
}
|
||||
|
||||
if (ReadShaderCache(cache_path, source_size, source_hash, static_cast<u32>(*variant),
|
||||
out_modules)) {
|
||||
if (ReadShaderCache(cache_path, source_size, source_hash, out_modules)) {
|
||||
return LosslessStatus::Ok;
|
||||
}
|
||||
|
||||
const LosslessStatus translate_status = TranslateAll(spans, out_modules, *variant);
|
||||
const LosslessStatus translate_status = TranslateAll(spans, out_modules);
|
||||
if (translate_status != LosslessStatus::Ok) {
|
||||
return translate_status;
|
||||
}
|
||||
@@ -522,7 +494,6 @@ LosslessStatus LoadShaderModules(ShaderModules& out_modules, bool allow_fp16, bo
|
||||
.source_size = source_size,
|
||||
.source_hash = source_hash,
|
||||
.module_count = static_cast<u32>(out_modules.size()),
|
||||
.variant = static_cast<u32>(*variant),
|
||||
};
|
||||
if (!WriteShaderCache(cache_path, header, out_modules)) {
|
||||
void(Common::FS::RemoveFile(cache_path));
|
||||
@@ -534,7 +505,7 @@ LosslessStatus LoadShaderModules(ShaderModules& out_modules, bool allow_fp16, bo
|
||||
|
||||
LosslessStatus BuildShaderCache() {
|
||||
ShaderModules modules;
|
||||
return LoadShaderModules(modules, true);
|
||||
return LoadShaderModules(modules);
|
||||
}
|
||||
|
||||
bool RemoveInstalledLosslessDll() {
|
||||
|
||||
@@ -28,11 +28,6 @@ enum class LosslessStatus : u32 {
|
||||
using ShaderResources = std::map<u32, std::vector<u8>>;
|
||||
using ShaderModules = std::map<u32, std::vector<u32>>;
|
||||
|
||||
enum class ShaderVariant : u32 {
|
||||
NativeFp32 = 1,
|
||||
NativeFp16 = 2,
|
||||
};
|
||||
|
||||
namespace PerformanceShader {
|
||||
constexpr u32 MIPMAPS = 255;
|
||||
constexpr u32 GENERATE = 256;
|
||||
@@ -42,7 +37,6 @@ constexpr std::array<u32, 5> GAMMA{280, 282, 283, 284, 285};
|
||||
constexpr std::array<u32, 10> DELTA{280, 286, 287, 288, 289, 281, 294, 295, 296, 297};
|
||||
|
||||
constexpr u32 NATIVE_FP16_OFFSET = 49;
|
||||
constexpr u32 NATIVE_FP32_OFFSET = 98;
|
||||
} // namespace PerformanceShader
|
||||
|
||||
[[nodiscard]] std::filesystem::path GetLosslessDllPath();
|
||||
@@ -58,9 +52,7 @@ constexpr u32 NATIVE_FP32_OFFSET = 98;
|
||||
|
||||
[[nodiscard]] LosslessStatus BuildShaderCache();
|
||||
|
||||
[[nodiscard]] LosslessStatus LoadShaderModules(ShaderModules& out_modules,
|
||||
bool allow_fp16 = false,
|
||||
bool prefer_fp16 = false);
|
||||
[[nodiscard]] LosslessStatus LoadShaderModules(ShaderModules& out_modules);
|
||||
|
||||
bool RemoveInstalledLosslessDll();
|
||||
|
||||
|
||||
@@ -198,6 +198,10 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format,
|
||||
VkExtent2D guest_extent) {
|
||||
generated = false;
|
||||
|
||||
if (!shaders) {
|
||||
shaders.emplace(device);
|
||||
}
|
||||
|
||||
if (unavailable || !Settings::values.frame_gen.GetValue()) {
|
||||
if (chain) {
|
||||
scheduler.Finish();
|
||||
@@ -207,17 +211,14 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format,
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frame->storage_view) {
|
||||
if (!shaders->IsValid()) {
|
||||
unavailable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shaders) {
|
||||
shaders.emplace(device);
|
||||
if (!shaders->IsValid()) {
|
||||
unavailable = true;
|
||||
return;
|
||||
}
|
||||
if (!frame->storage_view) {
|
||||
warm_streak = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
peak_guest_extent.width = std::max(peak_guest_extent.width, guest_extent.width);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "common/settings.h"
|
||||
#include "video_core/frame_gen/lossless_dll.h"
|
||||
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
|
||||
#include "video_core/renderer_vulkan/present/util.h"
|
||||
@@ -10,16 +9,13 @@
|
||||
namespace Vulkan {
|
||||
|
||||
LsfgShaders::LsfgShaders(const Device& device) {
|
||||
if (!device.IsVulkanMemoryModelSupported() || !device.HasNullDescriptor()) {
|
||||
if (!device.IsVulkanMemoryModelSupported() || !device.HasNullDescriptor() ||
|
||||
!device.IsFloat16Supported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool allow_fp16 = device.IsFloat16Supported();
|
||||
const bool prefer_fp16 = allow_fp16 && Settings::values.frame_gen_fp16.GetValue();
|
||||
|
||||
VideoCore::FrameGen::ShaderModules code;
|
||||
if (VideoCore::FrameGen::LoadShaderModules(code, allow_fp16, prefer_fp16) !=
|
||||
VideoCore::FrameGen::LosslessStatus::Ok) {
|
||||
if (VideoCore::FrameGen::LoadShaderModules(code) != VideoCore::FrameGen::LosslessStatus::Ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <vulkan/vulkan_core.h>
|
||||
#include "common/settings.h"
|
||||
#include "video_core/framebuffer_config.h"
|
||||
#include "video_core/present.h"
|
||||
#include "video_core/renderer_vulkan/present/filters.h"
|
||||
@@ -87,13 +88,18 @@ void BlitScreen::SetWindowAdaptPass(const Device& device) {
|
||||
|
||||
void BlitScreen::PrepareFrame(const Device& device, Frame* frame,
|
||||
const Layout::FramebufferLayout& layout) {
|
||||
if (!window_adapt || (frame->width == layout.width && frame->height == layout.height)) {
|
||||
if (!window_adapt) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame->width != layout.width || frame->height != layout.height) {
|
||||
WaitIdle(device);
|
||||
} else if (!present_manager.NeedsStorage(frame, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WaitIdle(device);
|
||||
present_manager.RecreateFrame(frame, layout.width, layout.height, swapchain_view_format,
|
||||
window_adapt->GetRenderPass());
|
||||
window_adapt->GetRenderPass(), true);
|
||||
}
|
||||
|
||||
void BlitScreen::DrawToFrame(const Device& device, RasterizerVulkan& rasterizer, Frame* frame,
|
||||
@@ -120,16 +126,20 @@ void BlitScreen::DrawToFrame(const Device& device, RasterizerVulkan& rasterizer,
|
||||
swapchain_view_format = current_swapchain_view_format;
|
||||
}
|
||||
|
||||
const bool storage_required = Settings::values.frame_gen.GetValue();
|
||||
if (resource_update_required) {
|
||||
WaitIdle(device);
|
||||
SetWindowAdaptPass(device);
|
||||
|
||||
if (presentation_recreate_required) {
|
||||
present_manager.RecreateFrame(frame, layout.width, layout.height, swapchain_view_format,
|
||||
window_adapt->GetRenderPass());
|
||||
window_adapt->GetRenderPass(), storage_required);
|
||||
}
|
||||
|
||||
image_index = 0;
|
||||
} else if (present_manager.NeedsStorage(frame, storage_required)) {
|
||||
present_manager.RecreateFrame(frame, layout.width, layout.height, swapchain_view_format,
|
||||
window_adapt->GetRenderPass(), true);
|
||||
}
|
||||
|
||||
const VkExtent2D window_size{
|
||||
|
||||
@@ -29,9 +29,6 @@ static_assert(MAX_FRAMES_IN_FLIGHT <= LSFG_MAX_TARGETS);
|
||||
|
||||
bool CanStoreToFrame(const vk::PhysicalDevice& physical_device, VkFormat format) {
|
||||
#ifdef HAS_LSFG
|
||||
if (!Settings::values.frame_gen.GetValue()) {
|
||||
return false;
|
||||
}
|
||||
const VkFormatProperties props{physical_device.GetFormatProperties(format)};
|
||||
return (props.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
||||
#else
|
||||
@@ -160,6 +157,7 @@ PresentManager::PresentManager(const vk::Instance& instance_,
|
||||
.pNext = nullptr,
|
||||
.flags = VK_FENCE_CREATE_SIGNALED_BIT,
|
||||
});
|
||||
frame.storage_capable = storage_supported;
|
||||
free_queue.push_back(&frame);
|
||||
}
|
||||
|
||||
@@ -205,15 +203,22 @@ size_t PresentManager::MaxExtraFrames() const {
|
||||
return image_count - 1;
|
||||
}
|
||||
|
||||
bool PresentManager::NeedsStorage(const Frame* frame, bool required) const {
|
||||
return required && frame->storage_capable && !frame->storage_view;
|
||||
}
|
||||
|
||||
void PresentManager::RecreateFrame(Frame* frame, u32 width, u32 height, VkFormat image_view_format,
|
||||
VkRenderPass rd) {
|
||||
VkRenderPass rd, bool storage) {
|
||||
auto& dld = device.GetLogical();
|
||||
|
||||
frame->width = width;
|
||||
frame->height = height;
|
||||
|
||||
const VkImageUsageFlags storage_usage =
|
||||
storage_supported ? static_cast<VkImageUsageFlags>(VK_IMAGE_USAGE_STORAGE_BIT) : 0;
|
||||
const bool with_storage = storage && frame->storage_capable;
|
||||
VkImageUsageFlags storage_usage = 0;
|
||||
if (with_storage) {
|
||||
storage_usage = VK_IMAGE_USAGE_STORAGE_BIT;
|
||||
}
|
||||
|
||||
frame->image = memory_allocator.CreateImage({
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
|
||||
@@ -264,7 +269,7 @@ void PresentManager::RecreateFrame(Frame* frame, u32 width, u32 height, VkFormat
|
||||
});
|
||||
|
||||
frame->storage_view = vk::ImageView{};
|
||||
if (storage_supported) {
|
||||
if (with_storage) {
|
||||
frame->storage_view = dld.CreateImageView({
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
|
||||
@@ -36,6 +36,7 @@ struct Frame {
|
||||
vk::CommandBuffer cmdbuf;
|
||||
vk::Semaphore render_ready;
|
||||
vk::Fence present_done;
|
||||
bool storage_capable{};
|
||||
};
|
||||
|
||||
class PresentManager {
|
||||
@@ -57,7 +58,9 @@ public:
|
||||
|
||||
/// Recreates the present frame to match the provided parameters
|
||||
void RecreateFrame(Frame* frame, u32 width, u32 height, VkFormat image_view_format,
|
||||
VkRenderPass rd);
|
||||
VkRenderPass rd, bool storage);
|
||||
|
||||
[[nodiscard]] bool NeedsStorage(const Frame* frame, bool required) const;
|
||||
|
||||
/// Waits for the present thread to finish presenting all queued frames.
|
||||
void WaitPresent();
|
||||
|
||||
+14
-19
@@ -73,6 +73,7 @@ class QPaintEngine;
|
||||
class QSurface;
|
||||
|
||||
constexpr int default_mouse_constrain_timeout = 10;
|
||||
constexpr int default_mouse_update_timeout = 5;
|
||||
|
||||
class RenderWidget : public QWidget {
|
||||
public:
|
||||
@@ -138,6 +139,10 @@ GRenderWindow::GRenderWindow(MainWindow* parent,
|
||||
|
||||
mouse_constrain_timer.setInterval(default_mouse_constrain_timeout);
|
||||
connect(&mouse_constrain_timer, &QTimer::timeout, this, &GRenderWindow::ConstrainMouse);
|
||||
|
||||
mouse_update_timer.setInterval(default_mouse_update_timeout);
|
||||
connect(&mouse_update_timer, &QTimer::timeout, this, &GRenderWindow::UpdateMouse);
|
||||
mouse_update_timer.start();
|
||||
}
|
||||
|
||||
void GRenderWindow::ExecuteProgram(std::size_t program_index) {
|
||||
@@ -487,7 +492,6 @@ void GRenderWindow::mousePressEvent(QMouseEvent* event) {
|
||||
input_subsystem->GetMouse()->PressMouseButton(button);
|
||||
input_subsystem->GetMouse()->PressButton(pos.x(), pos.y(), button);
|
||||
input_subsystem->GetMouse()->PressTouchButton(touch_x, touch_y, button);
|
||||
input_subsystem->GetMouse()->NotifyChanged();
|
||||
|
||||
emit MouseActivity();
|
||||
}
|
||||
@@ -508,7 +512,6 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
|
||||
input_subsystem->GetMouse()->MouseMove(touch_x, touch_y);
|
||||
input_subsystem->GetMouse()->TouchMove(touch_x, touch_y);
|
||||
input_subsystem->GetMouse()->Move(pos.x(), pos.y(), center_x, center_y);
|
||||
input_subsystem->GetMouse()->NotifyChanged();
|
||||
|
||||
// Center mouse for mouse panning
|
||||
if (Settings::values.mouse_panning && !Settings::values.mouse_enabled) {
|
||||
@@ -518,8 +521,7 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
|
||||
// Constrain mouse for mouse emulation with mouse panning
|
||||
if (Settings::values.mouse_panning && Settings::values.mouse_enabled) {
|
||||
const auto [clamped_mouse_x, clamped_mouse_y] = ClipToTouchScreen(x, y);
|
||||
QCursor::setPos(mapToGlobal(
|
||||
QPoint{static_cast<int>(clamped_mouse_x), static_cast<int>(clamped_mouse_y)}));
|
||||
QCursor::setPos(mapToGlobal(QPoint{int(clamped_mouse_x), int(clamped_mouse_y)}));
|
||||
}
|
||||
|
||||
mouse_constrain_timer.stop();
|
||||
@@ -534,16 +536,10 @@ void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
|
||||
const auto button = QtButtonToMouseButton(event->button());
|
||||
input_subsystem->GetMouse()->ReleaseButton(button);
|
||||
input_subsystem->GetMouse()->NotifyChanged();
|
||||
}
|
||||
|
||||
void GRenderWindow::ConstrainMouse() {
|
||||
if (QtCommon::emu_thread == nullptr || !Settings::values.mouse_panning) {
|
||||
mouse_constrain_timer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->isActiveWindow()) {
|
||||
if (QtCommon::emu_thread == nullptr || Settings::values.mouse_panning || !this->isActiveWindow()) {
|
||||
mouse_constrain_timer.stop();
|
||||
return;
|
||||
}
|
||||
@@ -552,22 +548,22 @@ void GRenderWindow::ConstrainMouse() {
|
||||
const auto pos = mapFromGlobal(QCursor::pos());
|
||||
const int new_pos_x = std::clamp(pos.x(), 0, width());
|
||||
const int new_pos_y = std::clamp(pos.y(), 0, height());
|
||||
|
||||
QCursor::setPos(mapToGlobal(QPoint{new_pos_x, new_pos_y}));
|
||||
return;
|
||||
} else {
|
||||
const int center_x = width() / 2;
|
||||
const int center_y = height() / 2;
|
||||
QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
|
||||
}
|
||||
}
|
||||
|
||||
const int center_x = width() / 2;
|
||||
const int center_y = height() / 2;
|
||||
|
||||
QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
|
||||
void GRenderWindow::UpdateMouse() {
|
||||
input_subsystem->GetMouse()->NotifyChanged(); // required to reset mouse once it's no longer moved
|
||||
}
|
||||
|
||||
void GRenderWindow::wheelEvent(QWheelEvent* event) {
|
||||
const int x = event->angleDelta().x();
|
||||
const int y = event->angleDelta().y();
|
||||
input_subsystem->GetMouse()->MouseWheelChange(x, y);
|
||||
input_subsystem->GetMouse()->NotifyChanged();
|
||||
}
|
||||
|
||||
void GRenderWindow::TouchBeginEvent(const QTouchEvent* event) {
|
||||
@@ -716,7 +712,6 @@ void GRenderWindow::focusOutEvent(QFocusEvent* event) {
|
||||
input_subsystem->GetKeyboard()->ReleaseAllKeys();
|
||||
input_subsystem->GetTouchScreen()->ReleaseAllTouch();
|
||||
input_subsystem->GetMouse()->ReleaseAllButtons();
|
||||
input_subsystem->GetMouse()->NotifyChanged();
|
||||
}
|
||||
|
||||
void GRenderWindow::resizeEvent(QResizeEvent* event) {
|
||||
|
||||
@@ -144,6 +144,7 @@ private:
|
||||
void TouchUpdateEvent(const QTouchEvent* event);
|
||||
void TouchEndEvent();
|
||||
void ConstrainMouse();
|
||||
void UpdateMouse();
|
||||
|
||||
void RequestCameraCapture();
|
||||
void OnCameraCapture(int requestId, const QImage& img);
|
||||
@@ -184,6 +185,7 @@ private:
|
||||
#endif
|
||||
|
||||
QTimer mouse_constrain_timer;
|
||||
QTimer mouse_update_timer;
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent* event) override;
|
||||
|
||||
+18
-68
@@ -3,6 +3,7 @@
|
||||
|
||||
// Static Qt on macOS doesn't use Vulkan
|
||||
// Other platforms do, and thus have conflicting VulkanMemoryAllocator symbols
|
||||
#include "core/launch_params.h"
|
||||
#if defined(QT_STATICPLUGIN) && !defined(__APPLE__)
|
||||
#undef VMA_IMPLEMENTATION
|
||||
#endif
|
||||
@@ -514,79 +515,28 @@ MainWindow::MainWindow(bool has_broken_vulkan)
|
||||
// to prevent the UI from blowing up.
|
||||
UpdateUITheme();
|
||||
|
||||
QStringList args = QApplication::arguments();
|
||||
if (QStringList args = QApplication::arguments(); args.size() >= 2) {
|
||||
QList<QByteArray> qba{};
|
||||
for (auto const& e : args)
|
||||
qba.push_back(e.toUtf8());
|
||||
std::vector<char*> args_cstr{};
|
||||
for (auto& e : qba)
|
||||
args_cstr.push_back(e.data());
|
||||
|
||||
if (args.size() < 2) {
|
||||
return;
|
||||
}
|
||||
auto const lp = Core::ParseLaunchParams(*QtCommon::system, args_cstr.size(), args_cstr.data(), nullptr);
|
||||
|
||||
QString game_path;
|
||||
bool should_launch_qlaunch = false;
|
||||
bool should_launch_hlaunch = false;
|
||||
bool should_launch_setup = false;
|
||||
bool has_gamepath = false;
|
||||
bool is_fullscreen = false;
|
||||
|
||||
// Preserves drag/drop functionality
|
||||
for (int i = 1; i < args.size(); ++i) {
|
||||
if (args[i] == QStringLiteral("-f")) {
|
||||
// Launch game in fullscreen mode
|
||||
is_fullscreen = true;
|
||||
} else if (args[i] == QStringLiteral("-u") && i < args.size() - 1) {
|
||||
// Launch game with a specific user
|
||||
int user_arg_idx = ++i;
|
||||
bool argument_ok;
|
||||
std::size_t selected_user = args[user_arg_idx].toUInt(&argument_ok);
|
||||
if (!argument_ok) {
|
||||
// try to look it up by username, only finds the first username that matches.
|
||||
std::string const user_arg_str = args[user_arg_idx].toStdString();
|
||||
auto const user_idx =
|
||||
QtCommon::system->GetProfileManager().GetUserIndex(user_arg_str);
|
||||
if (user_idx != std::nullopt) {
|
||||
selected_user = user_idx.value();
|
||||
} else {
|
||||
LOG_ERROR(Frontend, "Invalid user argument '{}'", user_arg_str);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (QtCommon::system->GetProfileManager().UserExistsIndex(selected_user)) {
|
||||
Settings::values.current_user = s32(selected_user);
|
||||
user_flag_cmd_line = true;
|
||||
} else {
|
||||
LOG_ERROR(Frontend, "Selected user {} doesn't exist", selected_user);
|
||||
}
|
||||
} else if (args[i] == QStringLiteral("-g") && i < args.size() - 1) {
|
||||
// Launch game at path
|
||||
game_path = args[++i];
|
||||
has_gamepath = true;
|
||||
} else if (args[i] == QStringLiteral("-input-profile") && i < args.size() - 1) {
|
||||
auto& players = Settings::values.players.GetValue();
|
||||
players[0].profile_name = args[++i].toStdString();
|
||||
} else if (args[i] == QStringLiteral("-qlaunch")) {
|
||||
should_launch_qlaunch = true;
|
||||
} else if (args[i] == QStringLiteral("-hlaunch")) {
|
||||
should_launch_hlaunch = true;
|
||||
} else if (args[i] == QStringLiteral("-setup")) {
|
||||
should_launch_setup = true;
|
||||
} else {
|
||||
game_path = args[i];
|
||||
has_gamepath = true;
|
||||
// Override fullscreen setting if gamepath or argument is provided
|
||||
if (!lp.filepath.empty() || lp.fullscreen) {
|
||||
ui->action_Fullscreen->setChecked(lp.fullscreen);
|
||||
}
|
||||
}
|
||||
|
||||
// Override fullscreen setting if gamepath or argument is provided
|
||||
if (has_gamepath || is_fullscreen) {
|
||||
ui->action_Fullscreen->setChecked(is_fullscreen);
|
||||
}
|
||||
|
||||
if (should_launch_setup) {
|
||||
LaunchFirmwareApplet(u64(Service::AM::AppletProgramId::Starter), std::nullopt);
|
||||
} else {
|
||||
if (!game_path.isEmpty()) {
|
||||
BootGame(game_path, ApplicationAppletParameters());
|
||||
} else if (should_launch_qlaunch) {
|
||||
if (!lp.filepath.empty()) {
|
||||
BootGame(QString::fromStdString(lp.filepath), ApplicationAppletParameters());
|
||||
} else if (lp.launch_setup) {
|
||||
LaunchFirmwareApplet(u64(Service::AM::AppletProgramId::Starter), std::nullopt);
|
||||
} else if (lp.launch_qlaunch) {
|
||||
LaunchFirmwareApplet(u64(Service::AM::AppletProgramId::QLaunch), std::nullopt);
|
||||
} else if (should_launch_hlaunch) {
|
||||
} else if (lp.launch_hlaunch) {
|
||||
std::filesystem::path const sd_dir =
|
||||
Common::FS::GetEdenPathString(Common::FS::EdenPath::SDMCDir);
|
||||
auto const hbl_path = (sd_dir / "atmosphere" / "hbl.nsp").string();
|
||||
|
||||
@@ -42,9 +42,6 @@ target_link_libraries(yuzu-cmd PRIVATE common core input_common frontend_common
|
||||
if (ENABLE_OPENGL)
|
||||
target_link_libraries(yuzu-cmd PRIVATE glad)
|
||||
endif()
|
||||
if (MSVC)
|
||||
target_link_libraries(yuzu-cmd PRIVATE getopt)
|
||||
endif()
|
||||
target_link_libraries(yuzu-cmd PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
|
||||
|
||||
create_resource("../../dist/eden.bmp" "yuzu_cmd/yuzu_icon.h" "yuzu_icon")
|
||||
|
||||
@@ -37,10 +37,16 @@ EmuWindow_SDL3::EmuWindow_SDL3(InputCommon::InputSubsystem* input_subsystem_, Co
|
||||
SDL_SetWindowTitle(this_->render_window, title.c_str());
|
||||
return 2000;
|
||||
}, this);
|
||||
mouse_timer = SDL_AddTimer(100, [](void *userdata, SDL_TimerID, Uint32) -> Uint32 {
|
||||
auto* this_ = (EmuWindow_SDL3*)userdata;
|
||||
this_->input_subsystem->GetMouse()->NotifyChanged();
|
||||
return 100;
|
||||
}, this);
|
||||
}
|
||||
|
||||
EmuWindow_SDL3::~EmuWindow_SDL3() {
|
||||
SDL_RemoveTimer(titlebar_timer);
|
||||
SDL_RemoveTimer(mouse_timer);
|
||||
system.HIDCore().UnloadInputDevices();
|
||||
input_subsystem->Shutdown();
|
||||
SDL_Quit();
|
||||
|
||||
@@ -84,6 +84,9 @@ protected:
|
||||
/// Periodic changer of titlebar (independent of event loop)
|
||||
SDL_TimerID titlebar_timer;
|
||||
|
||||
// Mouse resetter once it
|
||||
SDL_TimerID mouse_timer;
|
||||
|
||||
/// Is the window still open?
|
||||
bool is_open = true;
|
||||
|
||||
|
||||
+15
-189
@@ -10,6 +10,7 @@
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include "common/settings_enums.h"
|
||||
#include "core/launch_params.h"
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
@@ -52,12 +53,6 @@
|
||||
#include "common/windows/timer_resolution.h"
|
||||
#endif
|
||||
|
||||
#undef _UNICODE
|
||||
#include <getopt.h>
|
||||
#ifndef _MSC_VER
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
extern "C" {
|
||||
// tells Nvidia and AMD drivers to use the dedicated GPU by default on laptops with switchable
|
||||
@@ -67,25 +62,6 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void PrintHelp(const char* argv0) {
|
||||
std::cout << "Usage: " << argv0
|
||||
<< " [options] <filename>\n"
|
||||
"-c, --config Load the specified configuration file\n"
|
||||
"-f, --fullscreen Start in fullscreen mode\n"
|
||||
"-g, --game File path of the game to load\n"
|
||||
"-h, --help Display this help and exit\n"
|
||||
"-m, --multiplayer=nick:password@address:port"
|
||||
" Nickname, password, address and port for multiplayer\n"
|
||||
"-p, --program Pass following string as arguments to executable\n"
|
||||
"-u, --user Select a specific user profile from 0 to 7\n"
|
||||
"-d, --debug Run the GDB stub on a port from 1 to 65535\n"
|
||||
"-v, --version Output version information and exit\n";
|
||||
}
|
||||
|
||||
static void PrintVersion() {
|
||||
std::cout << "Eden " << Common::g_scm_branch << " " << Common::g_scm_desc << std::endl;
|
||||
}
|
||||
|
||||
static void OnStateChanged(const Network::RoomMember::State& state) {
|
||||
switch (state) {
|
||||
case Network::RoomMember::State::Idle:
|
||||
@@ -208,168 +184,18 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
|
||||
LOG_CRITICAL(Frontend, "Failed to get command line arguments");
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
#endif
|
||||
std::string filepath;
|
||||
std::optional<std::string> config_path{};
|
||||
std::string program_args;
|
||||
std::optional<int> selected_user{};
|
||||
std::optional<u16> override_gdb_port{};
|
||||
bool use_multiplayer = false;
|
||||
bool fullscreen = false;
|
||||
bool force_null_render = false;
|
||||
bool force_single_core = false;
|
||||
std::string nickname{};
|
||||
std::string password{};
|
||||
std::string address{};
|
||||
std::string input_profile{};
|
||||
std::optional<std::string> log_filter{};
|
||||
u16 port = Network::DefaultRoomPort;
|
||||
|
||||
static struct option long_options[] = {
|
||||
// clang-format off
|
||||
{"debug", no_argument, 0, 'd'},
|
||||
{"config", required_argument, 0, 'c'},
|
||||
{"fullscreen", no_argument, 0, 'f'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{"game", required_argument, 0, 'g'},
|
||||
{"multiplayer", required_argument, 0, 'm'},
|
||||
{"program", optional_argument, 0, 'p'},
|
||||
{"user", required_argument, 0, 'u'},
|
||||
{"version", no_argument, 0, 'v'},
|
||||
{"input-profile", no_argument, 0, 'i'},
|
||||
{"null-render", no_argument, 0, 'n'},
|
||||
{"singlecore", no_argument, 0, 's'},
|
||||
{"filter", no_argument, 0, 'x'},
|
||||
{0, 0, 0, 0},
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
while (optind < argc) {
|
||||
int arg = getopt_long(argc, argv, "g:fhvcip::c:u:d:", long_options, &option_index);
|
||||
if (arg != -1) {
|
||||
switch (char(arg)) {
|
||||
case 'd':
|
||||
override_gdb_port = uint16_t(atoi(optarg));
|
||||
break;
|
||||
case 'c':
|
||||
config_path = optarg;
|
||||
break;
|
||||
case 'f':
|
||||
fullscreen = true;
|
||||
LOG_INFO(Frontend, "Starting in fullscreen mode...");
|
||||
break;
|
||||
case 'h':
|
||||
PrintHelp(argv[0]);
|
||||
return SDL_APP_FAILURE;
|
||||
case 'g':
|
||||
filepath = std::string(optarg);
|
||||
break;
|
||||
case 'i': {
|
||||
input_profile = std::string(optarg);
|
||||
break;
|
||||
}
|
||||
case 'm': {
|
||||
use_multiplayer = true;
|
||||
const std::string str_arg(optarg);
|
||||
// regex to check if the format is nickname:password@ip:port
|
||||
// with optional :password
|
||||
const std::regex re("^([^:]+)(?::(.+))?@([^:]+)(?::([0-9]+))?$");
|
||||
if (!std::regex_match(str_arg, re)) {
|
||||
std::cout << "Wrong format for option --multiplayer\n";
|
||||
PrintHelp(argv[0]);
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
|
||||
std::smatch match;
|
||||
std::regex_search(str_arg, match, re);
|
||||
ASSERT(match.size() == 5);
|
||||
nickname = match[1];
|
||||
password = match[2];
|
||||
address = match[3];
|
||||
if (!match[4].str().empty()) {
|
||||
port = u16(std::strtoul(match[4].str().c_str(), nullptr, 0));
|
||||
}
|
||||
std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$");
|
||||
if (!std::regex_match(nickname, nickname_re)) {
|
||||
LOG_ERROR(Frontend, "Nickname is not valid. Must be 4 to 20 alphanumeric characters");
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
if (address.empty()) {
|
||||
LOG_ERROR(Frontend, "Address to room must not be empty");
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'p':
|
||||
program_args = argv[optind];
|
||||
++optind;
|
||||
break;
|
||||
case 'u':
|
||||
selected_user = atoi(optarg);
|
||||
break;
|
||||
case 'v':
|
||||
PrintVersion();
|
||||
return SDL_APP_FAILURE;
|
||||
case 'n':
|
||||
force_null_render = true;
|
||||
break;
|
||||
case 's':
|
||||
force_single_core = true;
|
||||
break;
|
||||
case 'x':
|
||||
log_filter = argv[optind];
|
||||
++optind;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
#ifdef _WIN32
|
||||
filepath = Common::UTF16ToUTF8(argv_w[optind]);
|
||||
#else
|
||||
filepath = argv[optind];
|
||||
wchar_t **argv_w = nullptr;
|
||||
#endif
|
||||
optind++;
|
||||
}
|
||||
}
|
||||
|
||||
SdlConfig config{config_path};
|
||||
|
||||
// apply the log_filter setting
|
||||
// the logger was initialized before and doesn't pick up the filter on its own
|
||||
Common::Log::Filter filter;
|
||||
filter.ParseFilterString(log_filter.value_or(Settings::values.log_filter.GetValue()));
|
||||
Common::Log::SetGlobalFilter(filter);
|
||||
|
||||
if (!program_args.empty()) {
|
||||
Settings::values.program_args = program_args;
|
||||
}
|
||||
|
||||
if (!input_profile.empty()) {
|
||||
auto& players = Settings::values.players.GetValue();
|
||||
players[0].profile_name = input_profile;
|
||||
}
|
||||
|
||||
if (selected_user.has_value()) {
|
||||
Settings::values.current_user = std::clamp(*selected_user, 0, 7);
|
||||
}
|
||||
|
||||
if (override_gdb_port.has_value()) {
|
||||
Settings::values.use_gdbstub = true;
|
||||
Settings::values.gdbstub_port = *override_gdb_port;
|
||||
}
|
||||
|
||||
if (force_single_core) {
|
||||
Settings::values.use_multi_core = false;
|
||||
}
|
||||
|
||||
if (force_null_render) {
|
||||
Settings::values.renderer_backend = Settings::RendererBackend::Null;
|
||||
}
|
||||
|
||||
Core::LaunchParams lp = Core::ParseLaunchParams(state->system, argc, argv, argv_w);
|
||||
#ifdef _WIN32
|
||||
LocalFree(argv_w);
|
||||
#endif
|
||||
SdlConfig config{lp.config_path};
|
||||
|
||||
if (filepath.empty()) {
|
||||
Core::ApplyLaunchParams(lp);
|
||||
|
||||
if (lp.filepath.empty()) {
|
||||
LOG_CRITICAL(Frontend, "Failed to load ROM: No ROM specified");
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
@@ -386,14 +212,14 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
|
||||
case Settings::RendererBackend::OpenGL_GLSL:
|
||||
case Settings::RendererBackend::OpenGL_GLASM:
|
||||
case Settings::RendererBackend::OpenGL_SPIRV:
|
||||
state->emu_window = std::make_unique<EmuWindow_SDL3_GL>(&input_subsystem, state->system, fullscreen);
|
||||
state->emu_window = std::make_unique<EmuWindow_SDL3_GL>(&input_subsystem, state->system, lp.fullscreen);
|
||||
break;
|
||||
#endif
|
||||
case Settings::RendererBackend::Vulkan:
|
||||
state->emu_window = std::make_unique<EmuWindow_SDL3_VK>(&input_subsystem, state->system, fullscreen);
|
||||
state->emu_window = std::make_unique<EmuWindow_SDL3_VK>(&input_subsystem, state->system, lp.fullscreen);
|
||||
break;
|
||||
case Settings::RendererBackend::Null:
|
||||
state->emu_window = std::make_unique<EmuWindow_SDL3_Null>(&input_subsystem, state->system, fullscreen);
|
||||
state->emu_window = std::make_unique<EmuWindow_SDL3_Null>(&input_subsystem, state->system, lp.fullscreen);
|
||||
break;
|
||||
default:
|
||||
LOG_CRITICAL(Frontend, "Invalid renderer backend");
|
||||
@@ -413,12 +239,12 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
|
||||
Service::AM::FrontendAppletParameters load_parameters{
|
||||
.applet_id = Service::AM::AppletId::Application,
|
||||
};
|
||||
const Core::SystemResultStatus load_result = state->system.Load(*state->emu_window, filepath, load_parameters);
|
||||
const Core::SystemResultStatus load_result = state->system.Load(*state->emu_window, lp.filepath, load_parameters);
|
||||
switch (load_result) {
|
||||
case Core::SystemResultStatus::Success:
|
||||
break; // Expected case
|
||||
case Core::SystemResultStatus::ErrorGetLoader:
|
||||
LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filepath);
|
||||
LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", lp.filepath);
|
||||
return SDL_APP_FAILURE;
|
||||
case Core::SystemResultStatus::ErrorLoader:
|
||||
LOG_CRITICAL(Frontend, "Failed to load ROM!");
|
||||
@@ -440,14 +266,14 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
|
||||
if (use_multiplayer) {
|
||||
if (lp.use_multiplayer) {
|
||||
if (auto member = Network::GetRoomMember().lock()) {
|
||||
member->BindOnChatMessageReceived(OnMessageReceived);
|
||||
member->BindOnStatusMessageReceived(OnStatusMessageReceived);
|
||||
member->BindOnStateChanged(OnStateChanged);
|
||||
member->BindOnError(OnNetworkError);
|
||||
LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, nickname);
|
||||
member->Join(nickname, address.c_str(), port, 0, Network::NoPreferredIP, password);
|
||||
LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", lp.address, lp.port, lp.nickname);
|
||||
member->Join(lp.nickname, lp.address.c_str(), lp.port, 0, Network::NoPreferredIP, lp.password);
|
||||
} else {
|
||||
LOG_ERROR(Network, "Could not access RoomMember");
|
||||
return SDL_APP_FAILURE;
|
||||
|
||||
Reference in New Issue
Block a user