From 1c4bbc8cde506665de1d1a7b36312c36a1e67a2b Mon Sep 17 00:00:00 2001 From: Silent Date: Sat, 22 Aug 2020 22:46:12 +0200 Subject: [PATCH 01/61] XInput: Do not use XINPUT_CAPS_FFB_SUPPORTED There are several reasons for this: 1. XINPUT_CAPS_FFB_SUPPORTED flag was introduced in Windows 8, and therefore only supported by XInput 1.4 2. Despite the name, this flag does NOT indicate whether normal rumble is supported. This flag is reserved for more complex force feedback, and according to MSDN it may have went unused on Windows. This fixes a future (the method is not used yet) bug where XInputControllerInterface::GetControllerRumbleMotorCount would erroreously report no rumble support. --- .../xinput_controller_interface.cpp | 18 ++---------------- .../xinput_controller_interface.h | 3 --- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/src/frontend-common/xinput_controller_interface.cpp b/src/frontend-common/xinput_controller_interface.cpp index 92866fd72..2833db177 100644 --- a/src/frontend-common/xinput_controller_interface.cpp +++ b/src/frontend-common/xinput_controller_interface.cpp @@ -39,11 +39,9 @@ bool XInputControllerInterface::Initialize(CommonHostInterface* host_interface) reinterpret_cast(GetProcAddress(m_xinput_module, reinterpret_cast(100))); if (!m_xinput_get_state) reinterpret_cast(GetProcAddress(m_xinput_module, "XInputGetState")); - m_xinput_get_capabilities = - reinterpret_cast(GetProcAddress(m_xinput_module, "XInputGetCapabilities")); m_xinput_set_state = reinterpret_cast(GetProcAddress(m_xinput_module, "XInputSetState")); - if (!m_xinput_get_state || !m_xinput_get_capabilities || !m_xinput_set_state) + if (!m_xinput_get_state || !m_xinput_set_state) { Log_ErrorPrintf("Failed to get XInput function pointers."); return false; @@ -72,7 +70,6 @@ void XInputControllerInterface::PollEvents() if (!cd.connected) { cd.connected = true; - UpdateCapabilities(i); OnControllerConnected(static_cast(i)); } @@ -155,17 +152,6 @@ void XInputControllerInterface::CheckForStateChanges(u32 index, const XINPUT_STA } } -void XInputControllerInterface::UpdateCapabilities(u32 index) -{ - ControllerData& cd = m_controllers[index]; - - XINPUT_CAPABILITIES caps = {}; - m_xinput_get_capabilities(index, 0, &caps); - cd.supports_rumble = (caps.Flags & 0x0001 /* XINPUT_CAPS_FFB_SUPPORTED */); - - Log_InfoPrintf("Controller %u: Rumble is %s", index, cd.supports_rumble ? "supported" : "not supported"); -} - void XInputControllerInterface::ClearBindings() { for (auto& it : m_controllers) @@ -277,7 +263,7 @@ u32 XInputControllerInterface::GetControllerRumbleMotorCount(int controller_inde if (static_cast(controller_index) >= XUSER_MAX_COUNT || !m_controllers[controller_index].connected) return 0; - return m_controllers[controller_index].supports_rumble ? NUM_RUMBLE_MOTORS : 0; + return NUM_RUMBLE_MOTORS; } void XInputControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths, diff --git a/src/frontend-common/xinput_controller_interface.h b/src/frontend-common/xinput_controller_interface.h index 5949922e6..5d02e1262 100644 --- a/src/frontend-common/xinput_controller_interface.h +++ b/src/frontend-common/xinput_controller_interface.h @@ -60,7 +60,6 @@ private: { XINPUT_STATE last_state = {}; bool connected = false; - bool supports_rumble = false; // Scaling value of 1.30f to 1.40f recommended when using recent controllers float axis_scale = 1.00f; @@ -74,7 +73,6 @@ private: using ControllerDataArray = std::array; void CheckForStateChanges(u32 index, const XINPUT_STATE& new_state); - void UpdateCapabilities(u32 index); bool HandleAxisEvent(u32 index, Axis axis, s32 value); bool HandleButtonEvent(u32 index, u32 button, bool pressed); @@ -82,7 +80,6 @@ private: HMODULE m_xinput_module{}; DWORD(WINAPI* m_xinput_get_state)(DWORD, XINPUT_STATE*); - DWORD(WINAPI* m_xinput_get_capabilities)(DWORD, DWORD, XINPUT_CAPABILITIES*); DWORD(WINAPI* m_xinput_set_state)(DWORD, XINPUT_VIBRATION*); std::mutex m_event_intercept_mutex; Hook::Callback m_event_intercept_callback; From 8def7420c4272c12a4d6b7a83ef5b6a84d0c3e65 Mon Sep 17 00:00:00 2001 From: Silent Date: Sat, 22 Aug 2020 22:52:54 +0200 Subject: [PATCH 02/61] XInput: Try to use XInput 1.3 if 1.4 is not present This simple change enables the use of Guide button for Windows 7 users, provided they have DirectX End-User Runtimes installed. XInput 9.1.0 does not have the hidden XInputGetStateEx export, so it was not possible to poll for Guide button. --- .../xinput_controller_interface.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/frontend-common/xinput_controller_interface.cpp b/src/frontend-common/xinput_controller_interface.cpp index 2833db177..076746f3b 100644 --- a/src/frontend-common/xinput_controller_interface.cpp +++ b/src/frontend-common/xinput_controller_interface.cpp @@ -23,15 +23,19 @@ ControllerInterface::Backend XInputControllerInterface::GetBackend() const bool XInputControllerInterface::Initialize(CommonHostInterface* host_interface) { - m_xinput_module = LoadLibraryA("xinput1_4.dll"); + m_xinput_module = LoadLibraryW(L"xinput1_4"); if (!m_xinput_module) { - m_xinput_module = LoadLibraryA("xinput9_1_0.dll"); - if (!m_xinput_module) - { - Log_ErrorPrintf("Failed to load XInput module."); - return false; - } + m_xinput_module = LoadLibraryW(L"xinput1_3"); + } + if (!m_xinput_module) + { + m_xinput_module = LoadLibraryW(L"xinput9_1_0"); + } + if (!m_xinput_module) + { + Log_ErrorPrintf("Failed to load XInput module."); + return false; } // Try the hidden version of XInputGetState(), which lets us query the guide button. From 7cc22e24d121a15a84c16ee17844026d84ec7a8b Mon Sep 17 00:00:00 2001 From: Blackbird88 Date: Sun, 23 Aug 2020 15:29:03 +0200 Subject: [PATCH 03/61] Star Wars - Dark Forces (SLUS-00297) gameini --- data/database/gamesettings.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/database/gamesettings.ini b/data/database/gamesettings.ini index 22b2b8ac1..a1d22d5f3 100644 --- a/data/database/gamesettings.ini +++ b/data/database/gamesettings.ini @@ -73,3 +73,8 @@ EnableInterlacing = true DisplayActiveStartOffset = 64 DisplayActiveEndOffset = 68 +# SLUS-00297 (Star Wars - Dark Forces (USA)) +[SLUS-00297] +DisableUpscaling = true +DisablePGXP = true +ForceDigitalController = true From b929afc33f2414d98d11d5d4fe7e1d0b59d2212a Mon Sep 17 00:00:00 2001 From: phoe-nix Date: Sun, 23 Aug 2020 22:22:28 +0800 Subject: [PATCH 04/61] Update duckstation-qt_zh-cn.ts (#762) * Update duckstation-qt_zh-cn.ts * Merge duckstation-qt_zh-cn.ts with master Co-authored-by: Connor McLaughlin --- .../translations/duckstation-qt_zh-cn.ts | 1700 ++++++++++++++--- 1 file changed, 1403 insertions(+), 297 deletions(-) diff --git a/src/duckstation-qt/translations/duckstation-qt_zh-cn.ts b/src/duckstation-qt/translations/duckstation-qt_zh-cn.ts index 06a36d008..a77ee4c53 100644 --- a/src/duckstation-qt/translations/duckstation-qt_zh-cn.ts +++ b/src/duckstation-qt/translations/duckstation-qt_zh-cn.ts @@ -128,21 +128,194 @@ - + Show Debug Menu + + + + + Use Debug Host GPU Device 使用调试主机GPU设备 - + Unchecked 不勾选 - + Enables the usage of debug devices and shaders for rendering APIs which support them. Should only be used when debugging the emulator. 允许使用调试设备和着色器渲染支持它们的API。只应在调试模拟器时使用。 + + AnalogController + + + + Controller %u switched to analog mode. + + + + + + Controller %u switched to digital mode. + + + + + Controller %u is locked to analog mode by the game. + + + + + Controller %u is locked to digital mode by the game. + + + + + LeftX + + + + + LeftY + + + + + RightX + + + + + RightY + + + + + Up + + + + + Down + + + + + Left + + + + + Right + + + + + Select + + + + + Start + 开始 + + + + Triangle + + + + + Cross + + + + + Circle + + + + + Square + + + + + L1 + + + + + L2 + + + + + R1 + + + + + R2 + + + + + L3 + + + + + R3 + + + + + Analog + + + + + Enable Analog Mode on Reset + 重启时启用模拟模式 + + + + Automatically enables analog mode when the console is reset/powered on. + + + + + Analog Axis Scale + 模拟摇杆灵敏度 + + + + Sets the analog stick axis scaling factor. A value between 1.30 and 1.40 is recommended when using recent controllers, e.g. DualShock 4, Xbox One Controller. + + + + + AudioBackend + + + Null (No Output) + + + + + Cubeb + + + + + SDL + + + AudioSettingsWidget @@ -172,11 +345,13 @@ + Sync To Output 同步到输出 + Start Dumping On Boot 启动时开始转储 @@ -192,7 +367,7 @@ - + Mute 静音 @@ -202,68 +377,72 @@ 100% - + Audio Backend 音频后端 - + The audio backend determines how frames produced by the emulator are submitted to the host. Cubeb provides the lowest latency, if you encounter issues, try the SDL backend. The null backend disables all host audio output. 音频后端决定如何将模拟器生成的帧提交到主机。Cubeb提供了最低的延迟, 如果遇到问题, 请尝试SDL后端。空后端禁用所有主机音频输出。 - + Buffer Size 缓存大小 - + The buffer size determines the size of the chunks of audio which will be pulled by the host. Smaller values reduce the output latency, but may cause hitches if the emulation speed is inconsistent. Note that the Cubeb backend uses smaller chunks regardless of this value, so using a low value here may not significantly change latency. 缓冲区大小决定主机将要拉入的音频块的大小。较小的值可减少输出延迟, 但如果仿真速度不一致, 则可能会导致挂起。请注意, Cubeb后端使用更小的块, 而不管这个值如何, 因此在这里使用较低的值可能不会显著改变延迟。 - + Checked 勾选 - Throttles the emulation speed based on the audio backend pulling audio frames. Sync will automatically be disabled if not running at 100% speed. - 根据音频后端拉取音频帧来限制模拟速度。如果没有以100%的速度运行, 同步将自动禁用。 + 根据音频后端拉取音频帧来限制模拟速度。如果没有以100%的速度运行, 同步将自动禁用。 - - + + Throttles the emulation speed based on the audio backend pulling audio frames. This helps to remove noises or crackling if emulation is too fast. Sync will automatically be disabled if not running at 100% speed. + + + + + Unchecked 不勾选 - + Start dumping audio to file as soon as the emulator is started. Mainly useful as a debug option. 一旦模拟器启动, 就开始将音频转储到文件中。主要用作调试选项。 - + Volume 音量 - + Controls the volume of the audio played on the host. Values are in percentage. 控制主机上播放的音频的音量。值以百分比表示。 - + Prevents the emulator from producing any audible sound. 防止模拟器产生任何可听见的声音。 - + Maximum latency: %1 frames (%2ms) 最大延迟: %1帧 (%2ms) - + %1% %1% @@ -272,8 +451,8 @@ AutoUpdaterDialog - - + + Automatic Updater 自动更新程序 @@ -303,46 +482,105 @@ 下载并安装 - + Skip This Update 跳过这个更新 - + Remind Me Later 稍后提醒我 - + Updater Error 更新错误 - + No updates are currently available. Please try again later. 当前没有可用的更新。请稍后再试。 - + Current Version: %1 (%2) 当前版本: %1 (%2) - + New Version: %1 (%2) 新版本: %1 (%2) - + + Loading... + + + + Downloading %1... 下载中 %1... - + Cancel 取消 + + CPUExecutionMode + + + Intepreter (Slowest) + 解释器 (最慢) + + + + Cached Interpreter (Faster) + 缓存解释器 (较快) + + + + Recompiler (Fastest) + 重编译器 (最快) + + + + CommonHostInterface + + + Are you sure you want to stop emulation? + + + + + The current state will be saved. + + + + + ConsoleRegion + + + Auto-Detect + + + + + NTSC-J (Japan) + + + + + NTSC-U (US) + + + + + PAL (Europe, Australia) + + + ConsoleSettingsWidget @@ -367,7 +605,7 @@ - + Fast Boot 快速启动 @@ -412,141 +650,333 @@ 将图像预加载到内存 - - + + Unchecked 不勾选 - + Patches the BIOS to skip the console's boot animation. Does not work with all games, but usually safe to enabled. 对BIOS应用补丁以跳过主机的启动动画, 不适用于所有游戏, 但通常可以安全启用。 - + Preload Image to RAM 将图像预加载到内存 - - Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay. - 将游戏图像加载到内存中。对于可能在游戏过程中变得不可靠的网络路径非常有用。 + + Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay. In some cases also eliminates stutter when games initiate audio track playback. + - + Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay. + 将游戏图像加载到内存中。对于可能在游戏过程中变得不可靠的网络路径非常有用。 + + + Select BIOS Image 选择BIOS文件 + + ControllerInterface + + + None + + + + + SDL + + + + + XInput + + + ControllerSettingsWidget - + Controller Type: 控制器类型: - + Load Profile 读取配置 - + Save Profile 保存配置 - + Clear All 清除全部 - + Clear Bindings 清除绑定 - + Are you sure you want to clear all bound controls? This can not be reversed. 确实要清除所有绑定控件吗?这是无法撤消的。 - - + + Rebind All 全部重新绑定 - + Are you sure you want to rebind all controls? All currently-bound controls will be irreversibly cleared. Rebinding will begin after confirmation. 是否确实要重新绑定所有控件?所有当前绑定的控件都将被不可逆转地清除。确认后将开始重新绑定。 - + Port %1 接口%1 - + Button Bindings: 按钮绑定: - + Axis Bindings: 轴绑定: - + Rumble Rumble - - - + + + Browse... 浏览... - + Select File 选择文件 - - + + Select path to input profile ini 选择输入配置文件ini的路径 - + New... 新建... - - + + Enter Input Profile Name 输入输入配置文件名 - - + + Error 错误 - + No name entered, input profile was not saved. 未输入名称, 未保存输入配置文件。 - + No path selected, input profile was not saved. 未选择路径, 未保存输入配置文件。 + + ControllerType + + + None + + + + + Digital Controller + + + + + Analog Controller (DualShock) + + + + + Namco GunCon + + + + + PlayStation Mouse + + + + + NeGcon + + + + + DigitalController + + + Up + + + + + Down + + + + + Left + + + + + Right + + + + + Select + + + + + Start + 开始 + + + + Triangle + + + + + Cross + + + + + Circle + + + + + Square + + + + + L1 + + + + + L2 + + + + + R1 + + + + + R2 + + + + + DiscRegion + + + NTSC-J (Japan) + + + + + NTSC-U (US) + + + + + PAL (Europe, Australia) + + + + + Other + + + + + DisplayCropMode + + + None + + + + + Only Overscan Area + 仅限过扫描区域 + + + + All Borders + + + + + GPURenderer + + + Hardware (D3D11) + + + + + Hardware (Vulkan) + + + + + Hardware (OpenGL) + + + + + Software + + + GPUSettingsWidget @@ -586,19 +1016,19 @@ - + Linear Upscaling 线性放大 - + Integer Upscaling 整数放大 - + VSync 垂直同步 @@ -626,7 +1056,7 @@ - + Disable Interlacing (force progressive render/scan) 禁用隔行扫描 (强制渐进式渲染/扫描) @@ -678,76 +1108,79 @@ 顶点缓存 - + + + CPU Mode + + + + Renderer 渲染器 - Chooses the backend to use for rendering tasks for the the console GPU. Depending on your system and hardware, Direct3D 11 and OpenGL hardware backends may be available. The software renderer offers the best compatibility, but is the slowest and does not offer any enhancements. - 选择用于呈现控制台GPU任务的后端。根据您的系统和硬件, Direct3D 11和OpenGL硬件后端可能可用。软件渲染器提供了最好的兼容性, 但速度最慢, 并且不提供任何增强功能。 + 选择用于呈现控制台GPU任务的后端。根据您的系统和硬件, Direct3D 11和OpenGL硬件后端可能可用。软件渲染器提供了最好的兼容性, 但速度最慢, 并且不提供任何增强功能。 - + Adapter 适配器 - - + + (Default) (默认) - If your system contains multiple GPUs or adapters, you can select which GPU you wish to use for the hardware renderers. This option is only supported in Direct3D and Vulkan, OpenGL will always use the default device. - 如果系统包含多个GPU或适配器, 则可以选择要将哪个GPU用于硬件渲染器。此选项仅在Direct3D和Vulkan中受支持, OpenGL将始终使用默认设备。 + 如果系统包含多个GPU或适配器, 则可以选择要将哪个GPU用于硬件渲染器。此选项仅在Direct3D和Vulkan中受支持, OpenGL将始终使用默认设备。 - + Aspect Ratio 高宽比 - + Changes the aspect ratio used to display the console's output to the screen. The default is 4:3 which matches a typical TV of the era. 更改用于在屏幕上显示控制台输出的纵横比。默认值是4:3, 与那个时代的典型电视相匹配。 - + Crop Mode 裁剪模式 - + Only Overscan Area 仅限过扫描区域 - Determines how much of the area typically not visible on a consumer TV set to crop/hide. Some games display content in the overscan area, or use it for screen effects and may not display correctly with the All Borders setting. Only Overscan offers a good compromise between stability and hiding black borders. - 确定用户电视机上通常不可见的区域有多少要裁剪/隐藏。有些游戏在“过扫描”区域显示内容, 或将其用于屏幕效果, 在“所有边框”设置下可能无法正确显示。只有过度扫描才能在稳定和隐藏黑边界之间提供一个很好的折衷方案。 + 确定用户电视机上通常不可见的区域有多少要裁剪/隐藏。有些游戏在“过扫描”区域显示内容, 或将其用于屏幕效果, 在“所有边框”设置下可能无法正确显示。只有过度扫描才能在稳定和隐藏黑边界之间提供一个很好的折衷方案。 - - + + + Unchecked 不勾选 - Forces the rendering and display of frames to progressive mode. This removes the "combing" effect seen in 480i games by rendering them in 480p. Not all games are compatible with this option, some require interlaced rendering or render interlaced internally. Usually safe to enable. - 强制以渐进模式渲染和显示帧。这将通过在480p中渲染480i游戏中的效果来移除它们。并非所有的游戏都与此选项兼容, 有些游戏需要隔行渲染或内部渲染。通常可以安全启用。 + 强制以渐进模式渲染和显示帧。这将通过在480p中渲染480i游戏中的效果来移除它们。并非所有的游戏都与此选项兼容, 有些游戏需要隔行渲染或内部渲染。通常可以安全启用。 - - + + @@ -755,19 +1188,16 @@ 勾选 - Uses bilinear texture filtering when displaying the console's framebuffer to the screen. Disabling filtering will producer a sharper, blockier/pixelated image. Enabling will smooth out the image. The option will be less noticable the higher the resolution scale. - 在将控制台帧缓冲区显示到屏幕时使用双线性纹理过滤。禁用过滤将生成更清晰、更块状/像素化的图像。启用将使图像平滑。分辨率越高, 选项就越不引人注意。 + 在将控制台帧缓冲区显示到屏幕时使用双线性纹理过滤。禁用过滤将生成更清晰、更块状/像素化的图像。启用将使图像平滑。分辨率越高, 选项就越不引人注意。 - Adds padding to the display area to ensure that the ratio between pixels on the host to pixels in the console is an integer number. May result in a sharper image in some 2D games. - 向显示区域添加填充, 以确保主机上的像素与控制台中的像素之间的比率为整数。在一些2D游戏中可能会产生更清晰的图像。 + 向显示区域添加填充, 以确保主机上的像素与控制台中的像素之间的比率为整数。在一些2D游戏中可能会产生更清晰的图像。 - Enables synchronization with the host display when possible. Enabling this option will provide better frame pacing and smoother motion with fewer duplicated frames. VSync is automatically disabled when it is not possible (e.g. running at non-100% speed). - 如果可能, 启用与主机显示的同步。启用此选项将以更少的重复帧提供更好的帧间距和更平滑的运动。当不可能时(例如, 以非100%速度运行), 垂直同步将自动禁用。 + 如果可能, 启用与主机显示的同步。启用此选项将以更少的重复帧提供更好的帧间距和更平滑的运动。当不可能时(例如, 以非100%速度运行), 垂直同步将自动禁用。 @@ -775,9 +1205,8 @@ 分辨率缩放 - Enables the upscaling of 3D objects rendered to the console's framebuffer. Only applies to the hardware backends. This option is usually safe, with most games looking fine at higher resolutions. Higher resolutions require a more powerful GPU. - 允许放大渲染到控制台帧缓冲区的三维对象。仅适用于硬件后端。这个选项通常是安全的, 大多数游戏在更高的分辨率下看起来很好。更高的分辨率需要更强大的GPU。 + 允许放大渲染到控制台帧缓冲区的三维对象。仅适用于硬件后端。这个选项通常是安全的, 大多数游戏在更高的分辨率下看起来很好。更高的分辨率需要更强大的GPU。 @@ -785,24 +1214,80 @@ 强制输出到控制台的帧缓冲区的颜色精度使用每个通道的全部8位精度。这会产生更好看的渐变, 但代价是使某些颜色看起来稍有不同。禁用该选项也会启用色彩抖动, 这会通过在这些像素周围应用图案来减少颜色之间的过渡。大多数游戏都与此选项兼容, 但也有一部分游戏不支持此选项, 并且在启用该选项后会产生中断效果。仅适用于硬件渲染器。 - Scales the dither pattern to the resolution scale of the emulated GPU. This makes the dither pattern much less obvious at higher resolutions. Usually safe to enable, and only supported by the hardware renderers. - 将着色彩抖动式缩放到模拟GPU的分辨率级别。这使得着色彩抖动式在更高分辨率下变得不那么明显。通常可以安全启用, 并且仅由硬件渲染器支持。 + 将着色彩抖动式缩放到模拟GPU的分辨率级别。这使得着色彩抖动式在更高分辨率下变得不那么明显。通常可以安全启用, 并且仅由硬件渲染器支持。 + + + Uses NTSC frame timings when the console is in PAL mode, forcing PAL games to run at 60hz. For most games which have a speed tied to the framerate, this will result in the game running approximately 17% faster. For variable frame rate games, it may not affect the speed. + 当游戏机处于PAL模式时使用NTSC帧计时, 强制PAL游戏以60hz运行。对于大多数速度与帧速率相关的游戏, 这将导致游戏运行速度大约快17%。对于可变帧速率游戏, 它可能不会影响速度。 + + + Smooths out the blockyness of magnified textures on 3D object by using bilinear filtering. Will have a greater effect on higher resolution scales. Only applies to the hardware renderers. + 利用双线性滤波消除三维物体上放大纹理的块状。会对更高分辨率的尺度产生更大的影响。目前, 在许多游戏中, 这个选项会在对象周围产生瑕疵, 需要进一步的工作。仅适用于硬件渲染器。 + + + Scales vertex positions in screen-space to a widescreen aspect ratio, essentially increasing the field of view from 4:3 to 16:9 in 3D games. <br>For 2D games, or games which use pre-rendered backgrounds, this enhancement will not work as expected. <b><u>May not be compatible with all games.</u></b> + 将屏幕空间中的顶点位置缩放到宽屏幕的纵横比, 基本上将3D游戏中的视野从4:3增加到16:9。<br>对于2D游戏, 或使用预渲染背景的游戏, 此增强将无法按预期工作。<b><u>可能不兼容所有游戏。</u></b> + + + + Chooses the backend to use for rendering the console/game visuals. <br>Depending on your system and hardware, Direct3D 11 and OpenGL hardware backends may be available. <br>The software renderer offers the best compatibility, but is the slowest and does not offer any enhancements. + + + + + If your system contains multiple GPUs or adapters, you can select which GPU you wish to use for the hardware renderers. <br>This option is only supported in Direct3D and Vulkan. OpenGL will always use the default device. + + + + + Determines how much of the area typically not visible on a consumer TV set to crop/hide. <br>Some games display content in the overscan area, or use it for screen effects. <br>May not display correctly with the "All Borders" setting. "Only Overscan" offers a good compromise between stability and hiding black borders. + + + + + Forces the rendering and display of frames to progressive mode. <br>This removes the "combing" effect seen in 480i games by rendering them in 480p. Usually safe to enable.<br> <b><u>May not be compatible with all games.</u></b> + + + + + Uses bilinear texture filtering when displaying the console's framebuffer to the screen. <br>Disabling filtering will producer a sharper, blockier/pixelated image. Enabling will smooth out the image. <br>The option will be less noticable the higher the resolution scale. + + + + + Adds padding to the display area to ensure that the ratio between pixels on the host to pixels in the console is an integer number. <br>May result in a sharper image in some 2D games. + + + + + Enable this option to match DuckStation's refresh rate with your current monitor or screen. VSync is automatically disabled when it is not possible (e.g. running at non-100% speed). + + + + + Setting this beyond 1x will enhance the resolution of rendered 3D polygons and lines. Only applies to the hardware backends. <br>This option is usually safe, with most games looking fine at higher resolutions. Higher resolutions require a more powerful GPU. + + + + + Scales the dither pattern to the resolution scale of the emulated GPU. This makes the dither pattern much less obvious at higher resolutions. <br>Usually safe to enable, and only supported by the hardware renderers. + - Uses NTSC frame timings when the console is in PAL mode, forcing PAL games to run at 60hz. For most games which have a speed tied to the framerate, this will result in the game running approximately 17% faster. For variable frame rate games, it may not affect the speed. - 当游戏机处于PAL模式时使用NTSC帧计时, 强制PAL游戏以60hz运行。对于大多数速度与帧速率相关的游戏, 这将导致游戏运行速度大约快17%。对于可变帧速率游戏, 它可能不会影响速度。 + Uses NTSC frame timings when the console is in PAL mode, forcing PAL games to run at 60hz. <br>For most games which have a speed tied to the framerate, this will result in the game running approximately 17% faster. <br>For variable frame rate games, it may not affect the speed. + - Smooths out the blockyness of magnified textures on 3D object by using bilinear filtering. Will have a greater effect on higher resolution scales. Only applies to the hardware renderers. - 利用双线性滤波消除三维物体上放大纹理的块状。会对更高分辨率的尺度产生更大的影响。目前, 在许多游戏中, 这个选项会在对象周围产生瑕疵, 需要进一步的工作。仅适用于硬件渲染器。 + Smooths out the blockyness of magnified textures on 3D object by using bilinear filtering. <br>Will have a greater effect on higher resolution scales. Only applies to the hardware renderers. + - Scales vertex positions in screen-space to a widescreen aspect ratio, essentially increasing the field of view from 4:3 to 16:9 in 3D games. <br>For 2D games, or games which use pre-rendered backgrounds, this enhancement will not work as expected. <b><u>May not be compatible with all games.</u></b> - 将屏幕空间中的顶点位置缩放到宽屏幕的纵横比, 基本上将3D游戏中的视野从4:3增加到16:9。<br>对于2D游戏, 或使用预渲染背景的游戏, 此增强将无法按预期工作。<b><u>可能不兼容所有游戏。</u></b> + Scales vertex positions in screen-space to a widescreen aspect ratio, essentially increasing the field of view from 4:3 to 16:9 in 3D games. <br>For 2D games, or games which use pre-rendered backgrounds, this enhancement will not work as expected. <br><b><u>May not be compatible with all games.</u></b> + @@ -825,70 +1310,108 @@ 当通过内存跟踪顶点失败时, 使用屏幕坐标作为备用。可提高PGXP兼容性。 - + + Tries to track vertex manipulation through the CPU. Some games require this option for PGXP to be effective. Very slow, and incompatible with the recompiler. + + + + (for 720p) (适合720p) - + (for 1080p) (适合1080p) - + (for 1440p) (适合1440p) - + (for 4K) (适合4K) - + Automatic based on window size 自动根据窗口大小 - + %1x%2 %1x%2 + + GameListCompatibilityRating + + + Unknown + + + + + Doesn't Boot + + + + + Crashes In Intro + + + + + Crashes In-Game + + + + + Graphical/Audio Issues + + + + + No Issues + + + GameListModel - + Type 类型 - + Code 编号 - + Title 标题 - + File Title 文件标题 - + Size 大小 - + Region 区域 - + Compatibility 兼容性 @@ -1021,97 +1544,162 @@ This will download approximately 4 megabytes over your current internet connecti Dialog - + + Properties + + + + Image Path: 文件路径: - + Game Code: 游戏编号: - + Title: 标题: - + Region: 区域: - + Compatibility: 兼容性: - + Upscaling Issues: 放大错误: - + Comments: 备注: - + Version Tested: 已测试版本: - + Set to Current 设置为当前 - + Tracks: 轨道: - + # # - + Mode 模式 - + Start 开始 - + Length 长度 - + Hash 哈希 - + + User Settings + + + + + GPU Settings + 视频设置 + + + + Crop Mode: + + + + + Aspect Ratio: + 纵横比: + + + + Widescreen Hack + 宽屏补丁 + + + + Controller Settings + 控制器设置 + + + + Controller 1 Type: + + + + + Controller 2 Type: + + + + + Compatibility Settings + + + + + Traits + + + + + Overrides + + + + + Display Active Offset: + + + + Compute Hashes 计算哈希 - + Verify Dump 验证转储 - + Export Compatibility Info 导出兼容性信息 - + Close 关闭 @@ -1121,31 +1709,106 @@ This will download approximately 4 megabytes over your current internet connecti 游戏属性 - %1 - %1 - %1 + %1 - + + + + + (unchanged) + + + + <not computed> <不兼容> - + Not yet implemented 尚未实施 - + Compatibility Info Export 兼容性信息导出 - + Press OK to copy to clipboard. 按“确定”复制到剪贴板。 + + GameSettingsTrait + + + Force Interpreter + + + + + Force Software Renderer + + + + + Enable Interlacing + + + + + Disable True Color + + + + + Disable Upscaling + + + + + Disable Scaled Dithering + + + + + Disable Widescreen + + + + + Disable PGXP + + + + + Disable PGXP Culling + + + + + Enable PGXP Vertex Cache + + + + + Enable PGXP CPU Mode + + + + + Force Digital Controller + + + + + Enable Recompiler Memory Exceptions + 启用重新编译内存异常 + + GeneralSettingsWidget @@ -1159,216 +1822,366 @@ This will download approximately 4 megabytes over your current internet connecti 行为 - - + + Pause On Start 开始时暂停 - - + + Confirm Power Off 确认关机 - - + + Save State On Exit 退出时保存即时存档 - - + + Load Devices From Save States 从即时存档读取设备 - - + + Start Fullscreen 全屏启动 - - + + Render To Main Window 渲染到主窗口 - - + + + Apply Per-Game Settings + + + + + Emulation Speed 模拟速度 - + 100% 100% - - + + Enable Speed Limiter 启用限速 - - + + Increase Timer Resolution 提高计时器分辨率 - + On-Screen Display 屏幕显示 - + Show Messages 显示消息 - - + + Show FPS 显示FPS - + Show Emulation Speed 显示模拟速度 - - + + Show VPS 显示VPS - - - - - - - + + Show Resolution + + + + + Miscellaneous + + + + + Controller Backend: + + + + + + + + + + + Checked 勾选 - + Determines whether a prompt will be displayed to confirm shutting down the emulator/game when the hotkey is pressed. 确定按下热键时是否显示确认关闭模拟器/游戏的提示。 - + Automatically saves the emulator state when powering down or exiting. You can then resume directly from where you left off next time. 关闭或退出时自动保存模拟器状态。然后你可以直接从你下次离开的地方继续。 - - - - - - - + + + + + + + Unchecked 不勾选 - + Automatically switches to fullscreen mode when a game is started. 游戏开始时自动切换到全屏模式。 - + Renders the display of the simulated console to the main window of the application, over the game list. If unchecked, the display will render in a separate window. 将模拟控制台的显示渲染到应用程序的主窗口, 显示在游戏列表上。如果未选中, 则显示将在单独的窗口中渲染。 - + Pauses the emulator when a game is started. 游戏开始时暂停模拟器。 - + When enabled, memory cards and controllers will be overwritten when save states are loaded. This can result in lost saves, and controller type mismatches. For deterministic save states, enable this option, otherwise leave disabled. 启用后, 记忆卡和控制器将在加载即时存档时被覆盖。这可能导致保存丢失, 以及控制器类型不匹配。对于确定性保存状态, 请启用此选项, 否则保持禁用状态。 - + + When enabled, per-game settings will be applied, and incompatible enhancements will be disabled. You should leave this option enabled except when testing enhancements with incompatible games. + + + + Throttles the emulation speed to the chosen speed above. If unchecked, the emulator will run as fast as possible, which may not be playable. 将模拟速度调节到上述选定速度。如果未选中, 模拟器将以最快的速度运行, 这可能无法播放。 - + Increases the system timer resolution when emulation is started to provide more accurate frame pacing. May increase battery usage on laptops. 在开始模拟时增加系统计时器分辨率, 以提供更精确的帧间距。可能会增加笔记本电脑的电池使用量。 - + Sets the target emulation speed. It is not guaranteed that this speed will be reached, and if not, the emulator will run as fast as it can manage. 设置目标模拟速度。不能保证达到这个速度, 如果不能, 模拟器将以它能管理的速度运行。 - + Show OSD Messages 显示屏幕消息 - + Shows on-screen-display messages when events occur such as save states being created/loaded, screenshots being taken, etc. 在发生事件(如正在创建/读取即时存档、正在拍摄屏幕截图等)时显示屏幕显示消息。 - + Shows the internal frame rate of the game in the top-right corner of the display. 在显示屏的右上角显示游戏的内部帧速率。 - + Shows the number of frames (or v-syncs) displayed per second by the system in the top-right corner of the display. 在显示屏右上角显示系统每秒显示的帧数(或垂直同步)。 - + Show Speed 显示速度 - + Shows the current emulation speed of the system in the top-right corner of the display as a percentage. 在显示屏右上角以百分比显示系统的当前模拟速度。 - - + + Controller Backend + + + + + Determines the backend which is used for controller input. Windows users may prefer to use XInput over SDL2 for compatibility. + + + + + Enable Discord Presence 启用Discord Presence - + Shows the game you are currently playing as part of your profile in Discord. 在Discord中显示您当前正在玩的游戏。 - - + + Enable Automatic Update Check 启用自动更新检查 - + Automatically checks for updates to the program on startup. Updates can be deferred until later or skipped entirely. 启动时自动检查程序的更新, 可以选择稍后更新或完全跳过。 - + %1% %1% + + Hotkeys + + + Fast Forward + + + + + Toggle Fast Forward + + + + + Toggle Fullscreen + + + + + Toggle Pause + + + + + Power Off System + + + + + Save Screenshot + + + + + Frame Step + + + + + Toggle Software Rendering + + + + + Toggle PGXP + + + + + Increase Resolution Scale + + + + + Decrease Resolution Scale + + + + + Load From Selected Slot + + + + + Save To Selected Slot + + + + + Select Previous Save Slot + + + + + Select Next Save Slot + + + + + Load Game State %u + + + + + Save Game State %u + + + + + Load Global State %u + + + + + Save Global State %u + + + + + Toggle Mute + + + + + Volume Up + + + + + Volume Down + + + InputBindingDialog @@ -1422,13 +2235,66 @@ This will download approximately 4 megabytes over your current internet connecti 按钮/轴... [%1] + + LogLevel + + + None + + + + + Error + 错误 + + + + Warning + + + + + Performance + + + + + Success + + + + + Information + 信息 + + + + Developer + + + + + Profile + + + + + Debug + + + + + Trace + + + MainWindow - - - + + + DuckStation DuckStation @@ -1439,363 +2305,408 @@ This will download approximately 4 megabytes over your current internet connecti - + Change Disc 换碟 - + + From Playlist... + + + + Load State 即时读档 - + Save State 即时存档 - + S&ettings 设置(&E) - + Theme 主题 - + Language 语言 - + &Help 帮助(&H) - + &Debug 调试(&D) - + Switch GPU Renderer 切换到GPU渲染器 - + Switch CPU Emulation Mode 切换到CPU模拟模式 - + + &View + + + + toolBar 工具栏 - + Start &Disc... 启动光盘(&D)... - + Start &BIOS 启动BIOS(&B) - + &Scan For New Games 扫描新游戏(&S) - + &Rescan All Games 重新扫描所有游戏(&R) - + Power &Off 关机(&O) - + &Reset 重启(&R) - + &Pause 暂停(&P) - + &Load State 即时读档(&L) - + &Save State 即时存档(&S) - + E&xit 退出(&X) - + C&onsole Settings... 主机设置(&O)... - + &Controller Settings... 控制器设置(&C)... - + &Hotkey Settings... 快捷键设置(&H)... - + &GPU Settings... 视频设置(&G)... - + Fullscreen 全屏 - + Resolution Scale 分辨率缩放 - + &GitHub Repository... GitHub库(&G)... - + &Issue Tracker... 问题反馈(&I)... - + &Discord Server... Discord服务器(&D)... - + Check for &Updates... 检查更新(&U)... - + &About... 关于(&A)... - + Change Disc... 换碟... - + Audio Settings... 音频设置... - + Game List Settings... 游戏列表设置... - + General Settings... 常规设置... - + Advanced Settings... 高级设置... - + Add Game Directory... 添加游戏路径... - + &Settings... 设置(&S)... - + From File... 从文件... - + From Game List... 从列表... - + Remove Disc 取出光盘 - + Resume State 恢复状态 - + Global State 全局状态 - + Show VRAM 显示显存 - + Dump CPU to VRAM Copies 将CPU转存到显存拷贝 - + Dump VRAM to CPU Copies 将显存转存到CPU拷贝 - + Dump Audio 导出音频 - + + Dump RAM... + + + + Show GPU State 显示GPU状态 - + Show CDROM State 显示光盘状态 - + Show SPU State 显示SPU状态 - + Show Timers State 显示计时器状态 - + Show MDEC State 显示MDEC状态 - + &Screenshot 截图(&S) - + &Memory Card Settings... 记忆卡设置(&M)... - + Resume 恢复 - + Resumes the last save state created. 恢复上次创建的保存状态。 - + + &Toolbar + + + + + &Status Bar + + + + + &Game List + + + + + System &Display + + + + + All File Types (*.bin *.img *.cue *.chd *.exe *.psexe *.psf);;Single-Track Raw Images (*.bin *.img);;Cue Sheets (*.cue);;MAME CHD Images (*.chd);;PlayStation Executables (*.exe *.psexe);;Portable Sound Format Files (*.psf);;Playlists (*.m3u) + + + + Failed to create host display device context. 无法创建主机显示设备内容。 - - + + Select Disc Image 选择光盘镜像 - + Properties... 属性... - + Open Containing Directory... 打开所在目录... - + Default Boot 默认启动 - + Fast Boot 快速启动 - + Full Boot 完全启动 - + Add Search Directory... 添加搜索目录... - + Language changed. Please restart the application to apply. 语言已更改, 请重新启动应用程序以应用。 - + + Destination File + + + + Default 默认 - + DarkFusion 黑色 - + QDarkStyle 深色 - + Updater Error 更新程序错误 - + <p>Sorry, you are trying to update a DuckStation version which is not an official GitHub release. To prevent incompatibilities, the auto-updater is only enabled on official builds.</p><p>To obtain an official build, please follow the instructions under "Downloading and Running" at the link below:</p><p><a href="https://github.com/stenzek/duckstation/">https://github.com/stenzek/duckstation/</a></p> <p>抱歉, 您正在尝试更新非GitHub官方版本的DuckStation版本。为防止不兼容, 自动更新程序仅在正式版本上启用。</p><p>要获取正式版本,请按照下面链接中的"下载并运行"下的说明进行操作:</p><p><a href="https://github.com/stenzek/duckstation/">https://github.com/stenzek/duckstation/</a>lt;/p> - + Automatic updating is not supported on the current platform. 当前平台不支持自动更新。 @@ -1803,41 +2714,183 @@ This will download approximately 4 megabytes over your current internet connecti MemoryCardSettingsWidget - + + All Memory Card Types (*.mcd *.mcr *.mc) + 全部记忆卡类型 (*.mcd *.mcr *.mc) + + + + Shared Settings + 共用设置 + + + + + Use Single Card For Playlist + 为列表使用单个记忆卡 + + + + Checked + 勾选 + + + + When using a playlist (m3u) and per-game (title) memory cards, a single memory card will be used for all discs. If unchecked, a separate card will be used for each disc. + 当使用列表 (m3u) 并且每个游戏使用独立记忆卡 (标题) 存储卡时, 所有光盘都将使用单个记忆卡。如果未勾选, 则每个光盘将使用各自的记忆卡。 + + + If one of the "separate card per game" memory card modes is chosen, these memory cards will be saved to the memcards directory. 如果选择了"每个游戏独立记忆卡"的存储卡模式, 这些存储卡将保存到memcards目录中。 - + Open... 打开... - + Memory Card %1 记忆卡%1 - + Memory Card Type: 记忆卡类型: - + Browse... 浏览... - + Shared Memory Card Path: 共用记忆卡路径: - + Select path to memory card image 选择记忆卡文件的路径 + + MemoryCardType + + + No Memory Card + + + + + Shared Between All Games + + + + + Separate Card Per Game (Game Code) + + + + + Separate Card Per Game (Game Title) + + + + + NamcoGunCon + + + Trigger + + + + + A + + + + + B + + + + + OSDMessage + + + System reset. + + + + + Loading state from '%s'... + + + + + Loading state from '%s' failed. Resetting. + + + + + Saving state to '%s' failed. + + + + + State saved to '%s'. + + + + + PGXP is incompatible with the software renderer, disabling PGXP. + + + + + PGXP CPU mode is incompatible with the recompiler, using Cached Interpreter instead. + + + + + Speed limiter enabled. + + + + + Speed limiter disabled. + + + + + Volume: Muted + + + + + + + Volume: %d%% + + + + + Loaded input profile from '%s' + + + + + Failed to save screenshot to '%s' + + + + + Screenshot saved to '%s'. + + + QObject @@ -1851,12 +2904,12 @@ This will download approximately 4 megabytes over your current internet connecti 初始化主机接口失败, 无法继续。 - + Failed to open URL 无法打开URL - + Failed to open URL. The URL was: %1 @@ -1868,42 +2921,62 @@ URL: %1 QtHostInterface - + + Game Save %1 (%2) + + + + + Game Save %1 (Empty) + + + + + Global Save %1 (%2) + + + + + Global Save %1 (Empty) + + + + Resume 恢复 - + Load State 即时读档 - + Resume (%1) 恢复(%1) - + %1 Save %2 (%3) "%1保存%2 (%3) - + Game 游戏 - + Delete Save States... 删除即时存档... - + Confirm Save State Deletion 确认删除即时存档 - + Are you sure you want to delete all save states for %1? The saves will not be recoverable. @@ -2043,4 +3116,37 @@ The saves will not be recoverable. 推荐 + + System + + + Save state is incompatible: expecting version %u but state is version %u. + + + + + Failed to open CD image from save state: '%s'. + + + + + Per-game memory card cannot be used for slot %u as the running game has no code. Using shared card instead. + + + + + Per-game memory card cannot be used for slot %u as the running game has no title. Using shared card instead. + + + + + Memory card path for slot %u is missing, using default. + + + + + Game changed, reloading memory cards. + + + From 9254fc9e639af88cc972ac1c55d7b773b3afbecb Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 23 Aug 2020 18:56:30 +1000 Subject: [PATCH 05/61] GPU/HW: Fix UB with reverse subtract and texture filtering on --- src/core/gpu_hw_shadergen.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/core/gpu_hw_shadergen.cpp b/src/core/gpu_hw_shadergen.cpp index 118dfe51e..ee1817367 100644 --- a/src/core/gpu_hw_shadergen.cpp +++ b/src/core/gpu_hw_shadergen.cpp @@ -594,7 +594,7 @@ std::string GPU_HW_ShaderGen::GenerateBatchFragmentShader(GPU_HW::BatchRenderMod WriteHeader(ss); DefineMacro(ss, "TRANSPARENCY", transparency != GPU_HW::BatchRenderMode::TransparencyDisabled); DefineMacro(ss, "TRANSPARENCY_ONLY_OPAQUE", transparency == GPU_HW::BatchRenderMode::OnlyOpaque); - DefineMacro(ss, "TRANSPARENCY_ONLY_TRANSPARENCY", transparency == GPU_HW::BatchRenderMode::OnlyTransparent); + DefineMacro(ss, "TRANSPARENCY_ONLY_TRANSPARENT", transparency == GPU_HW::BatchRenderMode::OnlyTransparent); DefineMacro(ss, "TEXTURED", textured); DefineMacro(ss, "PALETTE", actual_texture_mode == GPU::TextureMode::Palette4Bit || @@ -889,18 +889,23 @@ void BilinearSampleFromVRAM(uint4 texpage, float2 coords, float4 uv_limits, } else { - #if TRANSPARENCY_ONLY_TRANSPARENCY + #if TRANSPARENCY_ONLY_TRANSPARENT discard; #endif #if TRANSPARENCY_ONLY_OPAQUE - // We don't output the second color here because it's not used. + // We don't output the second color here because it's not used (except for filtering). o_col0 = float4(color, oalpha); - #elif USE_DUAL_SOURCE - o_col0 = float4(color, oalpha); - o_col1 = float4(0.0, 0.0, 0.0, 1.0 - ialpha); + #if USE_DUAL_SOURCE + o_col1 = float4(0.0, 0.0, 0.0, 1.0 - ialpha); + #endif #else - o_col0 = float4(color, 1.0 - ialpha); + #if USE_DUAL_SOURCE + o_col0 = float4(color, oalpha); + o_col1 = float4(0.0, 0.0, 0.0, 1.0 - ialpha); + #else + o_col0 = float4(color, 1.0 - ialpha); + #endif #endif o_depth = oalpha * v_pos.z; From 5115c75f8850e9b9cdcbbffadf14c2a034de0a3f Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 23 Aug 2020 19:38:37 +1000 Subject: [PATCH 06/61] CommonHostInterface: Clear input map before changing interface Should hopefully fix the crash when changing controller backends. --- src/frontend-common/common_host_interface.cpp | 16 +++++++++++----- src/frontend-common/common_host_interface.h | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index b29b205fb..73b7a87d3 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -461,6 +461,7 @@ void CommonHostInterface::UpdateControllerInterface() if (m_controller_interface) { + ClearInputMap(); m_controller_interface->Shutdown(); m_controller_interface.reset(); } @@ -939,15 +940,20 @@ bool CommonHostInterface::HandleHostMouseEvent(HostMouseButton button, bool pres void CommonHostInterface::UpdateInputMap(SettingsInterface& si) { - m_keyboard_input_handlers.clear(); - m_mouse_input_handlers.clear(); - if (m_controller_interface) - m_controller_interface->ClearBindings(); - + ClearInputMap(); UpdateControllerInputMap(si); UpdateHotkeyInputMap(si); } +void CommonHostInterface::ClearInputMap() +{ + m_keyboard_input_handlers.clear(); + m_mouse_input_handlers.clear(); + m_controller_vibration_motors.clear(); + if (m_controller_interface) + m_controller_interface->ClearBindings(); +} + void CommonHostInterface::AddControllerRumble(u32 controller_index, u32 num_motors, ControllerRumbleCallback callback) { ControllerRumbleState rumble; diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index 0c110dac5..78007c3f3 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -217,6 +217,7 @@ protected: bool HandleHostKeyEvent(HostKeyCode code, bool pressed); bool HandleHostMouseEvent(HostMouseButton button, bool pressed); void UpdateInputMap(SettingsInterface& si); + void ClearInputMap(); void AddControllerRumble(u32 controller_index, u32 num_motors, ControllerRumbleCallback callback); void UpdateControllerRumble(); From 559dc23e4ed2752b6bb8c0f6bf9a5feaa1a7a945 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 23 Aug 2020 20:01:50 +1000 Subject: [PATCH 07/61] CDROM: Set playing bit in status after starting Fixes menu music in Army Men 3D. --- src/core/cdrom.cpp | 61 ++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/src/core/cdrom.cpp b/src/core/cdrom.cpp index 42bb87b9a..0c8c17716 100644 --- a/src/core/cdrom.cpp +++ b/src/core/cdrom.cpp @@ -1596,6 +1596,7 @@ void CDROM::BeginPlaying(u8 track_bcd, TickCount ticks_late /* = 0 */, bool afte m_secondary_status.ClearActiveBits(); m_secondary_status.motor_on = true; + m_secondary_status.playing_cdda = true; ClearSectorBuffers(); ResetAudioDecoder(); @@ -2241,42 +2242,38 @@ void CDROM::ProcessCDDASector(const u8* raw_sector, const CDImage::SubChannelQ& // For CDDA sectors, the whole sector contains the audio data. Log_DevPrintf("Read sector %u as CDDA", m_current_lba); - // These bits/reporting doesn't happen if we're reading with the CDDA mode bit set. - if (m_drive_state == DriveState::Playing) + // The reporting doesn't happen if we're reading with the CDDA mode bit set. + if (m_drive_state == DriveState::Playing && m_mode.report_audio) { - m_secondary_status.playing_cdda = true; - if (m_mode.report_audio) + const u8 frame_nibble = subq.absolute_frame_bcd >> 4; + if (m_last_cdda_report_frame_nibble != frame_nibble) { - const u8 frame_nibble = subq.absolute_frame_bcd >> 4; - if (m_last_cdda_report_frame_nibble != frame_nibble) + m_last_cdda_report_frame_nibble = frame_nibble; + + Log_DebugPrintf("CDDA report at track[%02x] index[%02x] rel[%02x:%02x:%02x]", subq.track_number_bcd, + subq.index_number_bcd, subq.relative_minute_bcd, subq.relative_second_bcd, + subq.relative_frame_bcd); + + ClearAsyncInterrupt(); + m_async_response_fifo.Push(m_secondary_status.bits); + m_async_response_fifo.Push(subq.track_number_bcd); + m_async_response_fifo.Push(subq.index_number_bcd); + if (subq.absolute_frame_bcd & 0x10) { - m_last_cdda_report_frame_nibble = frame_nibble; - - Log_DebugPrintf("CDDA report at track[%02x] index[%02x] rel[%02x:%02x:%02x]", subq.track_number_bcd, - subq.index_number_bcd, subq.relative_minute_bcd, subq.relative_second_bcd, - subq.relative_frame_bcd); - - ClearAsyncInterrupt(); - m_async_response_fifo.Push(m_secondary_status.bits); - m_async_response_fifo.Push(subq.track_number_bcd); - m_async_response_fifo.Push(subq.index_number_bcd); - if (subq.absolute_frame_bcd & 0x10) - { - m_async_response_fifo.Push(subq.relative_minute_bcd); - m_async_response_fifo.Push(0x80 | subq.relative_second_bcd); - m_async_response_fifo.Push(subq.relative_frame_bcd); - } - else - { - m_async_response_fifo.Push(subq.absolute_minute_bcd); - m_async_response_fifo.Push(subq.absolute_second_bcd); - m_async_response_fifo.Push(subq.absolute_frame_bcd); - } - - m_async_response_fifo.Push(0); // peak low - m_async_response_fifo.Push(0); // peak high - SetAsyncInterrupt(Interrupt::DataReady); + m_async_response_fifo.Push(subq.relative_minute_bcd); + m_async_response_fifo.Push(0x80 | subq.relative_second_bcd); + m_async_response_fifo.Push(subq.relative_frame_bcd); } + else + { + m_async_response_fifo.Push(subq.absolute_minute_bcd); + m_async_response_fifo.Push(subq.absolute_second_bcd); + m_async_response_fifo.Push(subq.absolute_frame_bcd); + } + + m_async_response_fifo.Push(0); // peak low + m_async_response_fifo.Push(0); // peak high + SetAsyncInterrupt(Interrupt::DataReady); } } From 8f54711c729813d5f2df8b6dda8ebe584c0da563 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 23 Aug 2020 22:47:49 +1000 Subject: [PATCH 08/61] Make additional strings in controller settings translatable --- src/core/analog_controller.cpp | 49 +++++++++++-------- src/core/digital_controller.cpp | 22 ++++++--- src/core/namco_guncon.cpp | 9 ++-- .../controllersettingswidget.cpp | 17 ++++--- src/duckstation-qt/mainwindow.cpp | 5 +- .../memorycardsettingswidget.cpp | 2 +- 6 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src/core/analog_controller.cpp b/src/core/analog_controller.cpp index b745386e3..76038d77c 100644 --- a/src/core/analog_controller.cpp +++ b/src/core/analog_controller.cpp @@ -466,25 +466,31 @@ std::optional AnalogController::StaticGetButtonCodeByName(std::string_view Controller::AxisList AnalogController::StaticGetAxisNames() { -#define A(n) \ - { \ -#n, static_cast < s32>(Axis::n) \ - } - - return {A(LeftX), A(LeftY), A(RightX), A(RightY)}; - -#undef A + return {{TRANSLATABLE("AnalogController", "LeftX"), static_cast(Axis::LeftX)}, + {TRANSLATABLE("AnalogController", "LeftY"), static_cast(Axis::LeftY)}, + {TRANSLATABLE("AnalogController", "RightX"), static_cast(Axis::RightX)}, + {TRANSLATABLE("AnalogController", "RightY"), static_cast(Axis::RightY)}}; } Controller::ButtonList AnalogController::StaticGetButtonNames() { -#define B(n) \ - { \ -#n, static_cast < s32>(Button::n) \ - } - return {B(Up), B(Down), B(Left), B(Right), B(Select), B(Start), B(Triangle), B(Cross), B(Circle), - B(Square), B(L1), B(L2), B(R1), B(R2), B(L3), B(R3), B(Analog)}; -#undef B + return {{TRANSLATABLE("AnalogController", "Up"), static_cast(Button::Up)}, + {TRANSLATABLE("AnalogController", "Down"), static_cast(Button::Down)}, + {TRANSLATABLE("AnalogController", "Left"), static_cast(Button::Left)}, + {TRANSLATABLE("AnalogController", "Right"), static_cast(Button::Right)}, + {TRANSLATABLE("AnalogController", "Select"), static_cast(Button::Select)}, + {TRANSLATABLE("AnalogController", "Start"), static_cast(Button::Start)}, + {TRANSLATABLE("AnalogController", "Triangle"), static_cast(Button::Triangle)}, + {TRANSLATABLE("AnalogController", "Cross"), static_cast(Button::Cross)}, + {TRANSLATABLE("AnalogController", "Circle"), static_cast(Button::Circle)}, + {TRANSLATABLE("AnalogController", "Square"), static_cast(Button::Square)}, + {TRANSLATABLE("AnalogController", "L1"), static_cast(Button::L1)}, + {TRANSLATABLE("AnalogController", "L2"), static_cast(Button::L2)}, + {TRANSLATABLE("AnalogController", "R1"), static_cast(Button::R1)}, + {TRANSLATABLE("AnalogController", "R2"), static_cast(Button::R2)}, + {TRANSLATABLE("AnalogController", "L3"), static_cast(Button::L3)}, + {TRANSLATABLE("AnalogController", "R3"), static_cast(Button::R3)}, + {TRANSLATABLE("AnalogController", "Analog"), static_cast(Button::Analog)}}; } u32 AnalogController::StaticGetVibrationMotorCount() @@ -495,11 +501,14 @@ u32 AnalogController::StaticGetVibrationMotorCount() Controller::SettingList AnalogController::StaticGetSettings() { static constexpr std::array settings = { - {{SettingInfo::Type::Boolean, "AutoEnableAnalog", "Enable Analog Mode on Reset", - "Automatically enables analog mode when the console is reset/powered on.", "false"}, - {SettingInfo::Type::Float, "AxisScale", "Analog Axis Scale", - "Sets the analog stick axis scaling factor. A value between 1.30 and 1.40 is recommended when using recent " - "controllers, e.g. DualShock 4, Xbox One Controller.", + {{SettingInfo::Type::Boolean, "AutoEnableAnalog", TRANSLATABLE("AnalogController", "Enable Analog Mode on Reset"), + TRANSLATABLE("AnalogController", "Automatically enables analog mode when the console is reset/powered on."), + "false"}, + {SettingInfo::Type::Float, "AxisScale", TRANSLATABLE("AnalogController", "Analog Axis Scale"), + TRANSLATABLE( + "AnalogController", + "Sets the analog stick axis scaling factor. A value between 1.30 and 1.40 is recommended when using recent " + "controllers, e.g. DualShock 4, Xbox One Controller."), "1.00f", "0.01f", "1.50f", "0.01f"}}}; return SettingList(settings.begin(), settings.end()); diff --git a/src/core/digital_controller.cpp b/src/core/digital_controller.cpp index 5444356e2..8ab5d7bbc 100644 --- a/src/core/digital_controller.cpp +++ b/src/core/digital_controller.cpp @@ -1,6 +1,7 @@ #include "digital_controller.h" #include "common/assert.h" #include "common/state_wrapper.h" +#include "host_interface.h" DigitalController::DigitalController() = default; @@ -155,13 +156,20 @@ Controller::AxisList DigitalController::StaticGetAxisNames() Controller::ButtonList DigitalController::StaticGetButtonNames() { -#define B(n) \ - { \ -#n, static_cast < s32>(Button::n) \ - } - return {B(Up), B(Down), B(Left), B(Right), B(Select), B(Start), B(Triangle), - B(Cross), B(Circle), B(Square), B(L1), B(L2), B(R1), B(R2)}; -#undef B + return {{TRANSLATABLE("DigitalController", "Up"), static_cast(Button::Up)}, + {TRANSLATABLE("DigitalController", "Down"), static_cast(Button::Down)}, + {TRANSLATABLE("DigitalController", "Left"), static_cast(Button::Left)}, + {TRANSLATABLE("DigitalController", "Right"), static_cast(Button::Right)}, + {TRANSLATABLE("DigitalController", "Select"), static_cast(Button::Select)}, + {TRANSLATABLE("DigitalController", "Start"), static_cast(Button::Start)}, + {TRANSLATABLE("DigitalController", "Triangle"), static_cast(Button::Triangle)}, + {TRANSLATABLE("DigitalController", "Cross"), static_cast(Button::Cross)}, + {TRANSLATABLE("DigitalController", "Circle"), static_cast(Button::Circle)}, + {TRANSLATABLE("DigitalController", "Square"), static_cast(Button::Square)}, + {TRANSLATABLE("DigitalController", "L1"), static_cast(Button::L1)}, + {TRANSLATABLE("DigitalController", "L2"), static_cast(Button::L2)}, + {TRANSLATABLE("DigitalController", "R1"), static_cast(Button::R1)}, + {TRANSLATABLE("DigitalController", "R2"), static_cast(Button::R2)}}; } u32 DigitalController::StaticGetVibrationMotorCount() diff --git a/src/core/namco_guncon.cpp b/src/core/namco_guncon.cpp index 8916c5282..b3bf49488 100644 --- a/src/core/namco_guncon.cpp +++ b/src/core/namco_guncon.cpp @@ -209,12 +209,9 @@ Controller::AxisList NamcoGunCon::StaticGetAxisNames() Controller::ButtonList NamcoGunCon::StaticGetButtonNames() { -#define B(n) \ - { \ -#n, static_cast < s32>(Button::n) \ - } - return {B(Trigger), B(A), B(B)}; -#undef B + return {{TRANSLATABLE("NamcoGunCon", "Trigger"), static_cast(Button::Trigger)}, + {TRANSLATABLE("NamcoGunCon", "A"), static_cast(Button::A)}, + {TRANSLATABLE("NamcoGunCon", "B"), static_cast(Button::B)}}; } u32 NamcoGunCon::StaticGetVibrationMotorCount() diff --git a/src/duckstation-qt/controllersettingswidget.cpp b/src/duckstation-qt/controllersettingswidget.cpp index c7c5d7914..76783c03f 100644 --- a/src/duckstation-qt/controllersettingswidget.cpp +++ b/src/duckstation-qt/controllersettingswidget.cpp @@ -174,6 +174,7 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin QGridLayout* layout = new QGridLayout(ui->bindings_container); const auto buttons = Controller::GetButtonNames(ctype); + const char* cname = Settings::GetControllerTypeName(ctype); InputBindingWidget* first_button = nullptr; InputBindingWidget* last_button = nullptr; @@ -196,7 +197,7 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin std::string section_name = StringUtil::StdStringFromFormat("Controller%d", index + 1); std::string key_name = StringUtil::StdStringFromFormat("Button%s", button_name.c_str()); - QLabel* label = new QLabel(QString::fromStdString(button_name), ui->bindings_container); + QLabel* label = new QLabel(qApp->translate(cname, button_name.c_str()), ui->bindings_container); InputButtonBindingWidget* button = new InputButtonBindingWidget(m_host_interface, std::move(section_name), std::move(key_name), ui->bindings_container); layout->addWidget(label, start_row + current_row, current_column); @@ -233,7 +234,7 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin std::string section_name = StringUtil::StdStringFromFormat("Controller%d", index + 1); std::string key_name = StringUtil::StdStringFromFormat("Axis%s", axis_name.c_str()); - QLabel* label = new QLabel(QString::fromStdString(axis_name), ui->bindings_container); + QLabel* label = new QLabel(qApp->translate(cname, axis_name.c_str()), ui->bindings_container); InputAxisBindingWidget* button = new InputAxisBindingWidget(m_host_interface, std::move(section_name), std::move(key_name), ui->bindings_container); layout->addWidget(label, start_row + current_row, current_column); @@ -282,13 +283,13 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin { std::string section_name = StringUtil::StdStringFromFormat("Controller%d", index + 1); std::string key_name = si.key; - const QString setting_tooltip = si.description ? QString::fromUtf8(si.description) : ""; + const QString setting_tooltip = si.description ? qApp->translate(cname, si.description) : QString(); switch (si.type) { case SettingInfo::Type::Boolean: { - QCheckBox* cb = new QCheckBox(tr(si.visible_name), ui->bindings_container); + QCheckBox* cb = new QCheckBox(qApp->translate(cname, si.visible_name), ui->bindings_container); cb->setToolTip(setting_tooltip); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, cb, std::move(section_name), std::move(key_name), si.BooleanDefaultValue()); @@ -306,7 +307,7 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin sb->setSingleStep(si.IntegerStepValue()); SettingWidgetBinder::BindWidgetToIntSetting(m_host_interface, sb, std::move(section_name), std::move(key_name), si.IntegerDefaultValue()); - layout->addWidget(new QLabel(tr(si.visible_name), ui->bindings_container), start_row, 0); + layout->addWidget(new QLabel(qApp->translate(cname, si.visible_name), ui->bindings_container), start_row, 0); layout->addWidget(sb, start_row, 1, 1, 3); start_row++; } @@ -321,7 +322,7 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin sb->setSingleStep(si.FloatStepValue()); SettingWidgetBinder::BindWidgetToFloatSetting(m_host_interface, sb, std::move(section_name), std::move(key_name), si.FloatDefaultValue()); - layout->addWidget(new QLabel(tr(si.visible_name), ui->bindings_container), start_row, 0); + layout->addWidget(new QLabel(qApp->translate(cname, si.visible_name), ui->bindings_container), start_row, 0); layout->addWidget(sb, start_row, 1, 1, 3); start_row++; } @@ -333,7 +334,7 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin le->setToolTip(setting_tooltip); SettingWidgetBinder::BindWidgetToStringSetting(m_host_interface, le, std::move(section_name), std::move(key_name), si.StringDefaultValue()); - layout->addWidget(new QLabel(tr(si.visible_name), ui->bindings_container), start_row, 0); + layout->addWidget(new QLabel(qApp->translate(cname, si.visible_name), ui->bindings_container), start_row, 0); layout->addWidget(le, start_row, 1, 1, 3); start_row++; } @@ -356,7 +357,7 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin hbox->addWidget(le, 1); hbox->addWidget(browse_button); - layout->addWidget(new QLabel(tr(si.visible_name), ui->bindings_container), start_row, 0); + layout->addWidget(new QLabel(qApp->translate(cname, si.visible_name), ui->bindings_container), start_row, 0); layout->addLayout(hbox, start_row, 1, 1, 3); start_row++; } diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 97e67ef50..e28c7d6ad 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -27,10 +27,11 @@ #include #include -static constexpr char DISC_IMAGE_FILTER[] = +static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP( + "MainWindow", "All File Types (*.bin *.img *.cue *.chd *.exe *.psexe *.psf);;Single-Track Raw Images (*.bin *.img);;Cue Sheets " "(*.cue);;MAME CHD Images (*.chd);;PlayStation Executables (*.exe *.psexe);;Portable Sound Format Files " - "(*.psf);;Playlists (*.m3u)"; + "(*.psf);;Playlists (*.m3u)"); ALWAYS_INLINE static QString getWindowTitle() { diff --git a/src/duckstation-qt/memorycardsettingswidget.cpp b/src/duckstation-qt/memorycardsettingswidget.cpp index 63ee1eaae..6df775b40 100644 --- a/src/duckstation-qt/memorycardsettingswidget.cpp +++ b/src/duckstation-qt/memorycardsettingswidget.cpp @@ -11,7 +11,7 @@ #include #include -static constexpr char MEMORY_CARD_IMAGE_FILTER[] = "All Memory Card Types (*.mcd *.mcr *.mc)"; +static constexpr char MEMORY_CARD_IMAGE_FILTER[] = QT_TRANSLATE_NOOP("MemoryCardSettingsWidget", "All Memory Card Types (*.mcd *.mcr *.mc)"); MemoryCardSettingsWidget::MemoryCardSettingsWidget(QtHostInterface* host_interface, QWidget* parent, SettingsDialog* dialog) From 421c65ea690bcdfc06135b18c41ac180f72c6e28 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Mon, 24 Aug 2020 00:22:56 +1000 Subject: [PATCH 09/61] GPU/Vulkan: Fix reverse subtract blending with texture filtering Fixes pointer in Final Fantasy VII with texture filtering. --- src/core/gpu_hw_vulkan.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/gpu_hw_vulkan.cpp b/src/core/gpu_hw_vulkan.cpp index 12f2b0ae8..efc51ef25 100644 --- a/src/core/gpu_hw_vulkan.cpp +++ b/src/core/gpu_hw_vulkan.cpp @@ -683,7 +683,9 @@ bool GPU_HW_Vulkan::CompilePipelines() gpbuilder.SetBlendAttachment( 0, true, VK_BLEND_FACTOR_ONE, m_supports_dual_source_blend ? VK_BLEND_FACTOR_SRC1_ALPHA : VK_BLEND_FACTOR_SRC_ALPHA, - (static_cast(transparency_mode) == TransparencyMode::BackgroundMinusForeground) ? + (static_cast(transparency_mode) == TransparencyMode::BackgroundMinusForeground && + static_cast(render_mode) != BatchRenderMode::TransparencyDisabled && + static_cast(render_mode) != BatchRenderMode::OnlyOpaque) ? VK_BLEND_OP_REVERSE_SUBTRACT : VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD); From 348f4f78eb2b1a8ee4a3a6223e2543f176f946ed Mon Sep 17 00:00:00 2001 From: Silent Date: Sun, 23 Aug 2020 16:35:25 +0200 Subject: [PATCH 10/61] Add ForceDigitalController for a handful of games --- data/database/gamesettings.ini | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/data/database/gamesettings.ini b/data/database/gamesettings.ini index a1d22d5f3..6b2a02c99 100644 --- a/data/database/gamesettings.ini +++ b/data/database/gamesettings.ini @@ -78,3 +78,63 @@ DisplayActiveEndOffset = 68 DisableUpscaling = true DisablePGXP = true ForceDigitalController = true + + +# SCUS-94302 (Destruction Derby (USA)) +[SCUS-94302] +ForceDigitalController = true + + +# SCUS-94900 (Crash Bandicoot (USA)) +[SCUS-94900] +ForceDigitalController = true + + +# SCUS-94350 (Destruction Derby 2 (USA)) +[SCUS-94350] +ForceDigitalController = true + + +# PCPX-96085 (Gran Turismo (Japan) (Demo 1)) +[PCPX-96085] +ForceDigitalController = true + + +# SLUS-00106 (Grand Theft Auto (USA)) +[SLUS-00106] +ForceDigitalController = true + + +# SLUS-00590 (Need for Speed - V-Rally (USA)) +[SLUS-00590] +ForceDigitalController = true + + +# SLUS-00403 (Rage Racer (USA)) +[SLUS-00403] +ForceDigitalController = true + + +# SCUS-94300 (Ridge Racer (USA)) +[SCUS-94300] +ForceDigitalController = true + + +# SLUS-00214 (Ridge Racer Revolution (USA)) +[SLUS-00214] +ForceDigitalController = true + + +# SLUS-00204 (Road & Track Presents - The Need for Speed (USA)) +[SLUS-00204] +ForceDigitalController = true + + +# SLUS-00006 (Tekken (USA)) +[SLUS-00006] +ForceDigitalController = true + + +# SLUS-00213 (Tekken 2 (USA)) +[SLUS-00213] +ForceDigitalController = true From 032127a7d6a8ba5664c19eb95d3935e865a663fe Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Mon, 24 Aug 2020 00:49:40 +1000 Subject: [PATCH 11/61] Qt: Fix widescreen hack value for game settings Also fixes an .ini entry being created on right click->properties. --- src/duckstation-qt/gamepropertiesdialog.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/duckstation-qt/gamepropertiesdialog.cpp b/src/duckstation-qt/gamepropertiesdialog.cpp index f72fe4bed..2b76d6298 100644 --- a/src/duckstation-qt/gamepropertiesdialog.cpp +++ b/src/duckstation-qt/gamepropertiesdialog.cpp @@ -242,12 +242,12 @@ void GamePropertiesDialog::populateGameSettings() } if (gs.gpu_widescreen_hack.has_value()) { - QSignalBlocker sb(m_ui.userControllerType2); - m_ui.userWidescreenHack->setCheckState(Qt::Checked); + QSignalBlocker sb(m_ui.userWidescreenHack); + m_ui.userWidescreenHack->setCheckState(gs.gpu_widescreen_hack.value() ? Qt::Checked : Qt::Unchecked); } else { - QSignalBlocker sb(m_ui.userControllerType2); + QSignalBlocker sb(m_ui.userWidescreenHack); m_ui.userWidescreenHack->setCheckState(Qt::PartiallyChecked); } } From 6ca7e1211141e5799355b28f25a7598967549337 Mon Sep 17 00:00:00 2001 From: Anderson_Cardoso <43047877+andercard0@users.noreply.github.com> Date: Sun, 23 Aug 2020 17:31:16 -0300 Subject: [PATCH 12/61] Update duckstation-qt_pt-br.ts Translation notes | Not translatable: - Compatibility Settings Tab as well the options below * Force Interpreter * Disable true color * Disable PGXP Etc.. --- .../translations/duckstation-qt_pt-br.ts | 749 +++++++++++------- 1 file changed, 473 insertions(+), 276 deletions(-) diff --git a/src/duckstation-qt/translations/duckstation-qt_pt-br.ts b/src/duckstation-qt/translations/duckstation-qt_pt-br.ts index 451a3f85a..4fc6adda8 100644 --- a/src/duckstation-qt/translations/duckstation-qt_pt-br.ts +++ b/src/duckstation-qt/translations/duckstation-qt_pt-br.ts @@ -177,11 +177,13 @@ + Sync To Output Sincronizar + Start Dumping On Boot Despejar Audio ao Iniciar @@ -197,7 +199,7 @@ - + Mute Mudo @@ -207,68 +209,72 @@ 100% - + Audio Backend Opção de Audio - + The audio backend determines how frames produced by the emulator are submitted to the host. Cubeb provides the lowest latency, if you encounter issues, try the SDL backend. The null backend disables all host audio output. As opções disponiveis determinam como o jogo irá reproduzir os sons; Cubed, fornece menor latência (atraso), se tiver problemas tente usar a opção SDL. A opção Nulo desativa o som do jogo completamente no emulador. - + Buffer Size Tamanho do Buffer - + The buffer size determines the size of the chunks of audio which will be pulled by the host. Smaller values reduce the output latency, but may cause hitches if the emulation speed is inconsistent. Note that the Cubeb backend uses smaller chunks regardless of this value, so using a low value here may not significantly change latency. O Tamanho do Buffer determina o quão preciso será o som no emulador.Valores menores reduzem a latência de saída, mas podem causar problemas se a velocidade da emulação for inconsistente.Usar a opção Cubed implica em valores menores independente da latência o que não fará muita diferença final. - + Checked Marcado - Throttles the emulation speed based on the audio backend pulling audio frames. Sync will automatically be disabled if not running at 100% speed. + A sincronização será desativada automaticamente se não estiver funcionando a 100% da velocidade. + + + + Throttles the emulation speed based on the audio backend pulling audio frames. This helps to remove noises or crackling if emulation is too fast. Sync will automatically be disabled if not running at 100% speed. A sincronização será desativada automaticamente se não estiver funcionando a 100% da velocidade. - - + + Unchecked Desmarcado - + Start dumping audio to file as soon as the emulator is started. Mainly useful as a debug option. Inicia o despejo do audio para um arquivo assim que o emulador é iniciado. Útil só em caso de depuração. - + Volume Volume - + Controls the volume of the audio played on the host. Values are in percentage. Controla o volume do aúdio. Valores são mostrados em porcentagem. - + Prevents the emulator from producing any audible sound. Previne o emulador de produzir qualquer som. - + Maximum latency: %1 frames (%2ms) Latência Máxima:%1 frames (%2ms) - + %1% %1% @@ -377,7 +383,7 @@ - + Fast Boot Inicio Rápido @@ -422,28 +428,32 @@ Carregar jogo para RAM - - + + Unchecked Desmarcado - + Patches the BIOS to skip the console's boot animation. Does not work with all games, but usually safe to enabled. Pula a animação de inicio do console. Não funciona com todos os jogos, mas é seguro deixar marcado. - + Preload Image to RAM Pré-carregar Jogo para RAM - - Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay. + + Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay. In some cases also eliminates stutter when games initiate audio track playback. Carrega o jogo na memória RAM. Útil para evitar certas instabilidades durante o jogo. - + Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay. + Carrega o jogo na memória RAM. Útil para evitar certas instabilidades durante o jogo. + + + Select BIOS Image Escolha o Arquivo de BIOS @@ -451,108 +461,108 @@ ControllerSettingsWidget - + Controller Type: Tipo de Controle: - + Load Profile Carregar Perfil - + Save Profile Salvar Perfil - + Clear All Limpar Tudo - + Clear Bindings Limpar Atribuições - + Are you sure you want to clear all bound controls? This can not be reversed. Tem certeza que quer limpar todos os vinculos, isto não poderá ser desfeito. - - + + Rebind All Reatribuir Tudo - + Are you sure you want to rebind all controls? All currently-bound controls will be irreversibly cleared. Rebinding will begin after confirmation. Tem certeza que quer reatribuir todos os controles? Todas as mudanças feitas nos controles serão perdidas. A reatribuição se dará após a confirmação. - + Port %1 Port %1 - + Button Bindings: Atribuir Botões: - + Axis Bindings: Atribuir Analogicos: - + Rumble Vibração - - - + + + Browse... Procurar... - + Select File Escolha o Arquivo - - + + Select path to input profile ini Escolha o caminho para inserir o perfil do jogo - + New... Novo... - - + + Enter Input Profile Name Escolha um nome para o Perfil - - + + Error Erro - + No name entered, input profile was not saved. Nome não atribuido, configuração de controle não foi salva. - + No path selected, input profile was not saved. Caminho não atribuido, configuração de controle não foi salva. @@ -628,13 +638,13 @@ - + True Color Rendering (24-bit, disables dithering) Renderização em (24 Cores, desativa o efeito dithering) - + Scaled Dithering (scale dither pattern to resolution) Dithering Escalonado, (Escalona o padrão do dithering para a resolução) @@ -646,19 +656,19 @@ - + Force NTSC Timings (60hz-on-PAL) Força o temporizador rodar em NTSC (60hz em jogos EU) - + Bilinear Texture Filtering Filtragem de Textura Bilinear - + Widescreen Hack Hack para Telas Widescreen @@ -669,25 +679,25 @@ - + Geometry Correction Correção Geométrica - + Culling Correction Correção de Curvas - + Texture Correction Correção de Textura - + Vertex Cache Vertice Armazenado @@ -703,9 +713,8 @@ Rederizador - Chooses the backend to use for rendering tasks for the the console GPU. Depending on your system and hardware, Direct3D 11 and OpenGL hardware backends may be available. The software renderer offers the best compatibility, but is the slowest and does not offer any enhancements. - Escolhe a opção a ser usada para emular a GPU. Dependendo do seu sistema e hardware, As opções DX11 e OpenGL podem aparecer.O renderizador de software oferece a melhor compatibilidade, mas é o mais lento e não oferece nenhum aprimoramento. + Escolhe a opção a ser usada para emular a GPU. Dependendo do seu sistema e hardware, As opções DX11 e OpenGL podem aparecer.O renderizador de software oferece a melhor compatibilidade, mas é o mais lento e não oferece nenhum aprimoramento. @@ -713,19 +722,18 @@ Adaptador - If your system contains multiple GPUs or adapters, you can select which GPU you wish to use for the hardware renderers. This option is only supported in Direct3D and Vulkan, OpenGL will always use the default device. - Se você tem várias GPUs ,você poderá selecionar qual delas deseja usar para os renderizadores de hardware. Esta opção é suportada apenas no Direct3D e no Vulkan, OpenGL sempre usará o dispositivo padrão. + Se você tem várias GPUs ,você poderá selecionar qual delas deseja usar para os renderizadores de hardware. Esta opção é suportada apenas no Direct3D e no Vulkan, OpenGL sempre usará o dispositivo padrão. - - - - - - + + + + + + Unchecked Desmarcado @@ -755,19 +763,16 @@ Somente Área Renderizada - Determines how much of the area typically not visible on a consumer TV set to crop/hide. Some games display content in the overscan area, or use it for screen effects and may not display correctly with the All Borders setting. Only Overscan offers a good compromise between stability and hiding black borders. - Determina quanto da area normalmente não visivel em uma TV o usuário pode ver ou não.Alguns jogos mostram conteúdo fora desta area pré-determinada.Somente esta opção "overscan" (fora da área visivel) pode oferecer um boa estabilidade na hora de ocultar as tarjas (bordas)pretas quando ocorrem. + Determina quanto da area normalmente não visivel em uma TV o usuário pode ver ou não.Alguns jogos mostram conteúdo fora desta area pré-determinada.Somente esta opção "overscan" (fora da área visivel) pode oferecer um boa estabilidade na hora de ocultar as tarjas (bordas)pretas quando ocorrem. - Forces the rendering and display of frames to progressive mode. This removes the "combing" effect seen in 480i games by rendering them in 480p. Not all games are compatible with this option, some require interlaced rendering or render interlaced internally. Usually safe to enable. - Força a renderização e a exibição de quadros para o modo progressivo. Isso remove efeitos de "trepidação" Visto nos jogos 480i renderizando-os em 480p.Nem todos os jogos são compatíveis com esta opção, alguns requerem renderização entrelaçada internamente. Normalmente é seguro ativar. + Força a renderização e a exibição de quadros para o modo progressivo. Isso remove efeitos de "trepidação" Visto nos jogos 480i renderizando-os em 480p.Nem todos os jogos são compatíveis com esta opção, alguns requerem renderização entrelaçada internamente. Normalmente é seguro ativar. - Uses NTSC frame timings when the console is in PAL mode, forcing PAL games to run at 60hz. For most games which have a speed tied to the framerate, this will result in the game running approximately 17% faster. For variable frame rate games, it may not affect the speed. - Quando o console está no modo PAL - Geralmente jogos Europeus rodam a 50hz. força estes jogos a rodar em até 60hz sendo assim, resulta em um jogo mais rápido até 15%.Em jogos com taxas de quadro (fps) variável pode isto não afetará a velocidade na hora da jogatina. + Quando o console está no modo PAL - Geralmente jogos Europeus rodam a 50hz. força estes jogos a rodar em até 60hz sendo assim, resulta em um jogo mais rápido até 15%.Em jogos com taxas de quadro (fps) variável pode isto não afetará a velocidade na hora da jogatina. Forces the display of frames to progressive mode. This only affects the displayed image, the console will be unaware of the setting. If the game is internally producing interlaced frames, this option may not have any effect. Usually safe to enable. @@ -776,46 +781,41 @@ - - - + + + Checked Marcado - Uses bilinear texture filtering when displaying the console's framebuffer to the screen. Disabling filtering will producer a sharper, blockier/pixelated image. Enabling will smooth out the image. The option will be less noticable the higher the resolution scale. - Usa textura bilinear filtrando todo buffer para a tela principal.Desabilitar esta filtragem produzirá uma imagem mais nítida porém pixelada. Ativar irá deixar a imagem mais suave. Esta opção fica menos notável em resoluções mais altas. + Usa textura bilinear filtrando todo buffer para a tela principal.Desabilitar esta filtragem produzirá uma imagem mais nítida porém pixelada. Ativar irá deixar a imagem mais suave. Esta opção fica menos notável em resoluções mais altas. - Adds padding to the display area to ensure that the ratio between pixels on the host to pixels in the console is an integer number. May result in a sharper image in some 2D games. - Adiciona preenchimento na tela para garantir que a proporção entre pixels seja um número inteiro. Pode resultar em uma imagem mais nítida em alguns jogos 2D. + Adiciona preenchimento na tela para garantir que a proporção entre pixels seja um número inteiro. Pode resultar em uma imagem mais nítida em alguns jogos 2D. - Enables synchronization with the host display when possible. Enabling this option will provide better frame pacing and smoother motion with fewer duplicated frames. VSync is automatically disabled when it is not possible (e.g. running at non-100% speed). - Ativa a sincronização quando possível. A ativação dessa opção fornecerá melhor ritmo de quadros por segundo e movimento mais suave com menos quadros duplicados.<br><br>O V-Sync é desativado automaticamente quando não é possível usá-lo (por exemplo quando o jogo não estiver rodando a 100%). + Ativa a sincronização quando possível. A ativação dessa opção fornecerá melhor ritmo de quadros por segundo e movimento mais suave com menos quadros duplicados.<br><br>O V-Sync é desativado automaticamente quando não é possível usá-lo (por exemplo quando o jogo não estiver rodando a 100%). - + Resolution Scale Escala de Resolução - Enables the upscaling of 3D objects rendered to the console's framebuffer. Only applies to the hardware backends. This option is usually safe, with most games looking fine at higher resolutions. Higher resolutions require a more powerful GPU. - Permite o aumento de escala de objetos 3D renderizados, aplica-se apenas aos back-end de hardware é seguro usar essa opção na maioria dos jogos ficando melhor ainda em resoluções mais altas; Isto implica também no maior uso da sua Placa de Video. + Permite o aumento de escala de objetos 3D renderizados, aplica-se apenas aos back-end de hardware é seguro usar essa opção na maioria dos jogos ficando melhor ainda em resoluções mais altas; Isto implica também no maior uso da sua Placa de Video. - + Forces the precision of colours output to the console's framebuffer to use the full 8 bits of precision per channel. This produces nicer looking gradients at the cost of making some colours look slightly different. Disabling the option also enables dithering, which makes the transition between colours less sharp by applying a pattern around those pixels. Most games are compatible with this option, but there is a number which aren't and will have broken effects with it enabled. Only applies to the hardware renderers. Força a precisão das cores produz efeitos de gradientes mais agradável ao custo de fazer com que algumas cores pareçam um pouco diferentes. Desativar a opção também ativa alguns pontilhados, o que torna a transição entre cores menos nítida a maioria dos jogos é compatível com esta opção, os que não forem terão efeitos quebrados com a opção ativada. Aplica-se apenas aos renderizadores por hardware. - Scales the dither pattern to the resolution scale of the emulated GPU. This makes the dither pattern much less obvious at higher resolutions. Usually safe to enable, and only supported by the hardware renderers. - Escalona os 'ditherings' - pontilhados na imagem para a placa de Video.Torna a visão destes pontos muito menos visiveis em resoluções mais altas.Geralmente seguro ativar e suportado apenas pelos rederizadores por Hardware (ou seja usando sua placa de vídeo). + Escalona os 'ditherings' - pontilhados na imagem para a placa de Video.Torna a visão destes pontos muito menos visiveis em resoluções mais altas.Geralmente seguro ativar e suportado apenas pelos rederizadores por Hardware (ou seja usando sua placa de vídeo). Uses NTSC frame timings when the console is in PAL mode, forcing PAL games to run at 60hz. For most games which have a speed tied to the framerate, this will result in the game running approximately 17% faster. For variable frame rate games, it may not affect the framerate. @@ -834,32 +834,90 @@ Reduz 'tremeliques' nos poligonos tentando preservar os mesmos na hora da transferência para a memória. Funciona apenas se rederizado por hardware e pode não é compatível com todos os jogos. - Smooths out the blockyness of magnified textures on 3D object by using bilinear filtering. Will have a greater effect on higher resolution scales. Only applies to the hardware renderers. + Suaviza texturas ampliadas em objetos 3D usando filtragem bilinear. Terá efeito maior em resoluções mais altas. Aplica-se apenas aos rederizadores por hardware. + + + Scales vertex positions in screen-space to a widescreen aspect ratio, essentially increasing the field of view from 4:3 to 16:9 in 3D games. <br>For 2D games, or games which use pre-rendered backgrounds, this enhancement will not work as expected. <b><u>May not be compatible with all games.</u></b> + Escala as posições de vértices para uma proporção de aspecto esticado, aumentando o campo de visão de 4:3 para 16:9 em jogos 3D. <br>Para jogos 2D, ou jogos que usam fundos pré-rederizados, este aprimoramento não funcionará como esperado. <b><u>Pode não ser compatível com todos os jogos</u></b> + + + + Chooses the backend to use for rendering the console/game visuals. <br>Depending on your system and hardware, Direct3D 11 and OpenGL hardware backends may be available. <br>The software renderer offers the best compatibility, but is the slowest and does not offer any enhancements. + Escolhe a opção a ser usada para emular a GPU. Dependendo do seu sistema e hardware, As opções DX11 e OpenGL podem aparecer.O renderizador de software oferece a melhor compatibilidade, mas é o mais lento e não oferece nenhum aprimoramento. + + + + If your system contains multiple GPUs or adapters, you can select which GPU you wish to use for the hardware renderers. <br>This option is only supported in Direct3D and Vulkan. OpenGL will always use the default device. + Se você tem várias GPUs ,você poderá selecionar qual delas deseja usar para os renderizadores de hardware. Esta opção é suportada apenas no Direct3D e no Vulkan, OpenGL sempre usará o dispositivo padrão. + + + + Determines how much of the area typically not visible on a consumer TV set to crop/hide. <br>Some games display content in the overscan area, or use it for screen effects. <br>May not display correctly with the "All Borders" setting. "Only Overscan" offers a good compromise between stability and hiding black borders. + Determina quanto da area normalmente não visivel em uma TV o usuário pode ver ou não.Alguns jogos mostram conteúdo fora desta area pré-determinada.Somente esta opção "overscan" (fora da área visivel) pode oferecer um boa estabilidade na hora de ocultar as tarjas (bordas)pretas quando ocorrem. + + + + Forces the rendering and display of frames to progressive mode. <br>This removes the "combing" effect seen in 480i games by rendering them in 480p. Usually safe to enable.<br> <b><u>May not be compatible with all games.</u></b> + Força a renderização e a exibição de quadros para o modo progressivo. Isso remove efeitos de "trepidação" Visto nos jogos 480i renderizando-os em 480p.Nem todos os jogos são compatíveis com esta opção, alguns requerem renderização entrelaçada internamente. Normalmente é seguro ativar..</u></b> + + + + Uses bilinear texture filtering when displaying the console's framebuffer to the screen. <br>Disabling filtering will producer a sharper, blockier/pixelated image. Enabling will smooth out the image. <br>The option will be less noticable the higher the resolution scale. + Usa textura bilinear filtrando todo buffer para a tela principal.Desabilitar esta filtragem produzirá uma imagem mais nítida porém pixelada. Ativar irá deixar a imagem mais suave. Esta opção fica menos notável em resoluções mais altas. + + + + Adds padding to the display area to ensure that the ratio between pixels on the host to pixels in the console is an integer number. <br>May result in a sharper image in some 2D games. + Adiciona preenchimento na tela para garantir que a proporção entre pixels seja um número inteiro. Pode resultar em uma imagem mais nítida em alguns jogos 2D. + + + + Enable this option to match DuckStation's refresh rate with your current monitor or screen. VSync is automatically disabled when it is not possible (e.g. running at non-100% speed). + Habilite esta opção para combinar a taxa de atualização do emulador com seu monitor. O V-Sync (sincronização vertical) será desativado automaticamente quando não for possivel atingir 100% da velocidade. + + + + Setting this beyond 1x will enhance the resolution of rendered 3D polygons and lines. Only applies to the hardware backends. <br>This option is usually safe, with most games looking fine at higher resolutions. Higher resolutions require a more powerful GPU. + Aumentar a resolução para mais de 1x aumentará a resolução dos Poligonos e linhas em jogos 3D. Só é utilizável quando usado com placas de video dedicadas. <br> Geralmente é seguro ativar esta opção, deixando assim a maior parte dos jogos com vizual muito melhor em resoluções mais altas; Porém, utiliza mais da sua placa de Vídeo. + + + + Scales the dither pattern to the resolution scale of the emulated GPU. This makes the dither pattern much less obvious at higher resolutions. <br>Usually safe to enable, and only supported by the hardware renderers. + Escalona os 'ditherings' - pontilhados na imagem para a placa de Video.Torna a visão destes pontos muito menos visiveis em resoluções mais altas.Geralmente seguro ativar e suportado apenas pelos rederizadores por Hardware (ou seja usando sua placa de vídeo). + + + + Uses NTSC frame timings when the console is in PAL mode, forcing PAL games to run at 60hz. <br>For most games which have a speed tied to the framerate, this will result in the game running approximately 17% faster. <br>For variable frame rate games, it may not affect the speed. + Quando o console está no modo PAL - Geralmente jogos Europeus rodam a 50hz. força estes jogos a rodar em até 60hz sendo assim, resulta em um jogo mais rápido até 15%.Em jogos com taxas de quadro (fps) variável pode isto não afetará a velocidade na hora da jogatina. + + + + Smooths out the blockyness of magnified textures on 3D object by using bilinear filtering. <br>Will have a greater effect on higher resolution scales. Only applies to the hardware renderers. Suaviza texturas ampliadas em objetos 3D usando filtragem bilinear. Terá efeito maior em resoluções mais altas. Aplica-se apenas aos rederizadores por hardware. - - Scales vertex positions in screen-space to a widescreen aspect ratio, essentially increasing the field of view from 4:3 to 16:9 in 3D games. <br>For 2D games, or games which use pre-rendered backgrounds, this enhancement will not work as expected. <b><u>May not be compatible with all games.</u></b> + + Scales vertex positions in screen-space to a widescreen aspect ratio, essentially increasing the field of view from 4:3 to 16:9 in 3D games. <br>For 2D games, or games which use pre-rendered backgrounds, this enhancement will not work as expected. <br><b><u>May not be compatible with all games.</u></b> Escala as posições de vértices para uma proporção de aspecto esticado, aumentando o campo de visão de 4:3 para 16:9 em jogos 3D. <br>Para jogos 2D, ou jogos que usam fundos pré-rederizados, este aprimoramento não funcionará como esperado. <b><u>Pode não ser compatível com todos os jogos</u></b> - + Reduces "wobbly" polygons and "warping" textures that are common in PS1 games. <br>Only works with the hardware renderers. <b><u>May not be compatible with all games.</u></b> Reduz "tremeliques" nos poligonos tentando preservar os mesmos na hora da transferência para a memória. Funciona apenas se rederizado por hardware e pode não ser compatível com todos os jogos.</u></b> - + Increases the precision of polygon culling, reducing the number of holes in geometry. Requires geometry correction enabled. Aumenta a precisão das curvas nos poligonos, reduzindo o número de buracos na geometria do mesmo. Requer a Correção Geometrica ativada. - + Uses perspective-correct interpolation for texture coordinates and colors, straightening out warped textures. Requires geometry correction enabled. Utiliza interpolação corretiva em perspetiva para cordenadas e das cores na textura, endireitando as que estiverem distorcidas. Requer correção de geometria ativada. - + Uses screen coordinates as a fallback when tracking vertices through memory fails. May improve PGXP compatibility. Quando a correção de vertices falha, essa opção se encarrega de usar as coordenadas da tela para o rastreamento. Pode melhorar a compatibilidade com o PGXP. @@ -869,32 +927,32 @@ Tenta manipular o rastreamento dos vértices (extremidades) direto para o processador. Alguns jogos exigem esta opção para que o aprimoramento - PGXP. tenha o efeito desejado. Atenção, este modo é MUITO LENTO, e imcompativel com o recompilador se ativo. - + (for 720p) >(720p) - + (for 1080p) >(1080p) - + (for 1440p) >(1440p) - + (for 4K) >(4k) - + Automatic based on window size Automático, baseado no tamanho da janela aberta - + %1x%2 %1x%2 @@ -904,7 +962,7 @@ - + (Default) Padrão @@ -1071,97 +1129,162 @@ This will download approximately 4 megabytes over your current internet connecti - + + Properties + Propriedades + + + Image Path: Caminho da Imagem: - + Game Code: Código do Jogo: - + Title: Titulo: - + Region: Região: - + Compatibility: Compatibilidade: - + Upscaling Issues: Problemas Escalonamento: - + Comments: Comentários: - + Version Tested: Versão Testada: - + Set to Current Definir para Atual - + Tracks: Faixas: - + # # - + Mode Modo - + Start Iniciar - + Length Comprimento - + Hash Valores - + + User Settings + Configurações Personalizadas + + + + GPU Settings + Configurações da GPU + + + + Crop Mode: + Modo de Corte: + + + + Aspect Ratio: + Proporção e Aspecto: + + + + Widescreen Hack + Melhoria para Telas Panorâmicas + + + + Controller Settings + Configurações de Controle + + + + Controller 1 Type: + Opção Controle 1: + + + + Controller 2 Type: + Opção Controle 2: + + + + Compatibility Settings + Configurações de Compatibilidade + + + + Traits + Caracteristicas Individuais + + + + Overrides + Sobreposições + + + + Display Active Offset: + Opções de Deslocamento: + + + Compute Hashes Calcular Valores - + Verify Dump Validar Jogo - + Export Compatibility Info Exportar Informação de Compatibilidade - + Close Fechar @@ -1171,27 +1294,34 @@ This will download approximately 4 megabytes over your current internet connecti Propriedades do Jogo - %1 - %1 - %1 + %1 - + + + + + (unchanged) + (Inalterado) + + + <not computed> - + <não calculado> - + Not yet implemented Não Implementado Ainda - + Compatibility Info Export - + Press OK to copy to clipboard. Dê ok para copiar para área de transferência. @@ -1209,217 +1339,249 @@ This will download approximately 4 megabytes over your current internet connecti Comportamento - - + + Pause On Start Pausar ao Iniciar - - + + Confirm Power Off Confirmar ao Fechar - - + + Save State On Exit Salvar ao Fechar - - + + Load Devices From Save States Carregar a partir do estado salvo - - + + Start Fullscreen Iniciar em Tela Cheia - - + + Render To Main Window Carregar Jogo na janela principal - - + + + Apply Per-Game Settings + Usar Configs. Separadas por Jogo + + + + Emulation Speed Velocidade da emulação - + 100% 100% - - + + Enable Speed Limiter Ativa Limitador de Velocidade - - + + Increase Timer Resolution Aumentar Resolução em Tempo Real - + On-Screen Display Mensagens na Tela - + Show Messages Mostrar Mensagens - - + + Show FPS Mostar FPS - + Show Emulation Speed Mostrar velocidade de Emulação - - + + Show VPS Mostrar VPS - + Show Resolution Mostrar Resolução - - - - - - - + + Miscellaneous + Diversos + + + + Controller Backend: + Tipo de Controle: + + + + + + + + + + Checked Marcado - + Determines whether a prompt will be displayed to confirm shutting down the emulator/game when the hotkey is pressed. Determina se uma janela será mostrara para confirmar o fechamento do emulador ou jogo quando atalho é pressionado. - + Automatically saves the emulator state when powering down or exiting. You can then resume directly from where you left off next time. Salva automaticamente o estado do emulador ao desligar ou sair. Você pode retomar diretamente de onde parou na próxima vez. - - - - - - + + + + + + Unchecked Desmarcado - + Automatically switches to fullscreen mode when a game is started. Muda para o modo Tela Cheia assim que um Jogo é Iniciado. - + Renders the display of the simulated console to the main window of the application, over the game list. If unchecked, the display will render in a separate window. Renderiza o jogo na janela principal do emulador sob a janela da lista de jogos. Se desmarcado, o jogo irá rodar em uma janela separada. - + Pauses the emulator when a game is started. Pausa a emulação quando um jogo é iniciado. - + When enabled, memory cards and controllers will be overwritten when save states are loaded. This can result in lost saves, and controller type mismatches. For deterministic save states, enable this option, otherwise leave disabled. Quando ativado, os cartões de memória e os controles serão substituídos assim que os saves states forem carregados. Isso pode resultar em perda de Saves e incompatibilidade nos controles. sendo assim, deixe isto desativado. - + + When enabled, per-game settings will be applied, and incompatible enhancements will be disabled. You should leave this option enabled except when testing enhancements with incompatible games. + Quando ativadas, as configurações separadas por jogos serão aplicadas e os aprimoramentos incompatíveis serão desligados. Você deve deixar esta opção ativada exceto ao usar melhorias com jogos não compatíveis. + + + Throttles the emulation speed to the chosen speed above. If unchecked, the emulator will run as fast as possible, which may not be playable. Acelera a velocidade da emulação para a velocidade escolhida acima. Se desmarcado, o emulador será executado o mais rápido possível, pode ser que não seja possivel sequer jogar. - + Increases the system timer resolution when emulation is started to provide more accurate frame pacing. May increase battery usage on laptops. Aumenta a resolução em tempo real quando emulador é iniciado dando maior precisão nos quadros emulados. Pode aumentar o consumo de bateria em Laptops. - + Sets the target emulation speed. It is not guaranteed that this speed will be reached, and if not, the emulator will run as fast as it can manage. Ajusta a velocidade da emulação. Não é garantido que a velocidade será alcançada sendo assim o emulador irá rodar o mais rápido que pode. - + Show OSD Messages Mostar mensagens em Tela - + Shows on-screen-display messages when events occur such as save states being created/loaded, screenshots being taken, etc. - Mostrar as mensagens na tela quando eventos ocorrerem;Quando um SaveState é criado ou carregado, capturas de tela forem feitas etc. + Mostrar as mensagens na tela (canto superior esquerdo) quando eventos ocorrerem; Quando um SaveState é criado ou carregado, capturas de tela forem feitas etc. - + Shows the internal frame rate of the game in the top-right corner of the display. Mostra o FPS atual do jogo no topo superior direito da tela. - + Shows the number of frames (or v-syncs) displayed per second by the system in the top-right corner of the display. Mostra o FPS no canto superior direito da tela. - + Show Speed Mostrar Velocidade - + Shows the current emulation speed of the system in the top-right corner of the display as a percentage. Mostra a velocidade de emulação atual do sistema no canto superior direito da tela registrado em porcentagem. - - + + Controller Backend + Tipo de Controle + + + + Determines the backend which is used for controller input. Windows users may prefer to use XInput over SDL2 for compatibility. + Determina qual opção de controle será usada para o controle em uso. Para quem usa Windows dê preferência ao X-Input ao invés do SDL2 para melhor compatibilidade. + + + + Enable Discord Presence Ativar Presença Rica do Discord - + Shows the game you are currently playing as part of your profile in Discord. Mostra o jogo que estiver jogando em seu perfil no Discord quando logado. - - + + Enable Automatic Update Check Verificar Por Atualizações - + Automatically checks for updates to the program on startup. Updates can be deferred until later or skipped entirely. Verifica automaticamente por atualizações assim que o emulador for iniciado. Atualizações podem ser postergadas ou ignoradas completamente. - + %1% %1% @@ -1481,9 +1643,9 @@ This will download approximately 4 megabytes over your current internet connecti MainWindow - - - + + + DuckStation DuckStation @@ -1494,7 +1656,7 @@ This will download approximately 4 megabytes over your current internet connecti - + Change Disc Mudar Disco @@ -1516,7 +1678,7 @@ This will download approximately 4 megabytes over your current internet connecti S&ettings - Configurações + &Configurações @@ -1531,12 +1693,12 @@ This will download approximately 4 megabytes over your current internet connecti &Help - Ajuda + &Ajuda &Debug - Depurar + &Depurar @@ -1549,251 +1711,276 @@ This will download approximately 4 megabytes over your current internet connecti Mudar Modo de emulação para CPU - + + &View + &Ver + + + toolBar - + Start &Disc... Iniciar Disco... - + Start &BIOS Iniciar BIOS - + &Scan For New Games Escanear Jogos Novos - + &Rescan All Games Scanear Todos os Jogos - + Power &Off Desligar - + &Reset Reiniciar - + &Pause Pausar - + &Load State Carregar Estado - + &Save State Salvar Estado - + E&xit Sair - + C&onsole Settings... Configuração do Console - + &Controller Settings... Configuração de Controles... - + &Hotkey Settings... Configuração de Atalhos... - + &GPU Settings... Configuração da GPU - + Fullscreen Tela Cheia - + Resolution Scale Escala de Resolução - + &GitHub Repository... Repositório no Github... - + &Issue Tracker... Problemas Abertos... - + &Discord Server... Servidor no Discord... - + Check for &Updates... Checar por Atualizações... - + &About... Sobre... - + Change Disc... Mudar Disco... - + Audio Settings... Configurações de Audio... - + Game List Settings... Configurar lista de Jogos... - + General Settings... Configurações Gerais... - + Advanced Settings... Configurações Avançadas... - + Add Game Directory... Adicionar Diretório de Jogo... - + &Settings... Configurações... - + From File... Do Arquivo... - + From Game List... Da lista de Jogos... - + Remove Disc Remover Disco - + Resume State Resumir Estado - + Global State Estado Global - + Show VRAM Mostrar VRAM - + Dump CPU to VRAM Copies Despejar cópias do CPU para a VRAM - + Dump VRAM to CPU Copies Despejar cópias da VRAM para o CPU - + Dump Audio Despejar Audio - + Dump RAM... Despejar para RAM... - + Show GPU State Mostrar Estado da GPU - + Show CDROM State Mostrar estado do CD-Rom - + Show SPU State Mostrar estado do SPU - + Show Timers State Mostrar estado do Temporizador - + Show MDEC State Mostrar estado do MDEC - + &Screenshot Captura de Tela - + &Memory Card Settings... Configurações de Memory Card... - + Resume Resumir - + Resumes the last save state created. Resumir Último Estado Salvo + + + &Toolbar + &Barra de Ferramentas + + + + &Status Bar + Barra de &Status + + + + &Game List + &Caminho dos Jogos + + + + System &Display + Sistema e &Video + Failed to get window info from widget Falha ao tentar obter informação da janela - + Failed to create host display device context. Falha ao criar uma amostra de contexto da tela. @@ -1802,84 +1989,94 @@ This will download approximately 4 megabytes over your current internet connecti Falha ao tentar obter novas informações da janela - - + + All File Types (*.bin *.img *.cue *.chd *.exe *.psexe *.psf);;Single-Track Raw Images (*.bin *.img);;Cue Sheets (*.cue);;MAME CHD Images (*.chd);;PlayStation Executables (*.exe *.psexe);;Portable Sound Format Files (*.psf);;Playlists (*.m3u) + + + + + Select Disc Image Escolha uma imagem de Disco - + Properties... Propriedades... - + Open Containing Directory... Abrir diretório... - + Default Boot Inicio Padrão - + Fast Boot Inicio Rápido - + Full Boot Inicio Completo - + Add Search Directory... Adiciona um novo diretório... - + Language changed. Please restart the application to apply. Lingua Alterada. Reinicie para Aplicar!. - + Destination File Destino do Arquivo - + Default Padrão - + DarkFusion DarkFusion - + QDarkStyle Escuro - + Updater Error Erro na Atualização - + <p>Sorry, you are trying to update a DuckStation version which is not an official GitHub release. To prevent incompatibilities, the auto-updater is only enabled on official builds.</p><p>To obtain an official build, please follow the instructions under "Downloading and Running" at the link below:</p><p><a href="https://github.com/stenzek/duckstation/">https://github.com/stenzek/duckstation/</a></p> <p>Desculpe mas, Você está tentando atualizar uma versão não oficial do Duckstation Para evitarmos imcompatibilidade, o atualizador automático só poderá ser usado nas versões oficiais! </p><p>Para obtê-las, Siga as instruções de como e onde no link "Baixando e Rodando" conforme abaixo:</p><p><a href="https://github.com/stenzek/duckstation/">https://github.com/stenzek/duckstation/</a></p> - + Automatic updating is not supported on the current platform. Atualizções automáticas não são suportadas na plataforma atual. MemoryCardSettingsWidget + + + All Memory Card Types (*.mcd *.mcr *.mc) + Todos os Tipos de MC (*.mcd *.mcr *.mc) + Shared Settings @@ -1965,62 +2162,62 @@ The URL was: %1 QtHostInterface - + Game Save %1 (%2) Jogo Salvo %1 (%2) - + Game Save %1 (Empty) Jogo Salvo %1 (Vazio) - + Global Save %1 (%2) Compartimento Global %1 (%2) - + Global Save %1 (Empty) Compartimento Global %1 (Vazio) - + Resume Resumir - + Load State Carregar Estado - + Resume (%1) Resumir (%1) - + %1 Save %2 (%3) %1 Salvo %2 (%3) - + Game Jogo - + Delete Save States... Apagar Jogos Salvos... - + Confirm Save State Deletion Confirma deleção de Estado Salvo - + Are you sure you want to delete all save states for %1? The saves will not be recoverable. From 5f3642e9fd221524f48244e76a6146f08e41b4f1 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 25 Aug 2020 19:07:12 +1000 Subject: [PATCH 13/61] VulkanLoader: Search frameworks directory for libvulkan.dylib --- dep/vulkan-loader/src/vulkan_loader.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/dep/vulkan-loader/src/vulkan_loader.cpp b/dep/vulkan-loader/src/vulkan_loader.cpp index c1be6ae57..0d580c219 100644 --- a/dep/vulkan-loader/src/vulkan_loader.cpp +++ b/dep/vulkan-loader/src/vulkan_loader.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include "vulkan_loader.h" @@ -14,6 +16,10 @@ #include #endif +#ifdef __APPLE__ +#include +#endif + #define VULKAN_MODULE_ENTRY_POINT(name, required) PFN_##name name; #define VULKAN_INSTANCE_ENTRY_POINT(name, required) PFN_##name name; #define VULKAN_DEVICE_ENTRY_POINT(name, required) PFN_##name name; @@ -111,6 +117,25 @@ bool LoadVulkanLibrary() char* libvulkan_env = getenv("LIBVULKAN_PATH"); if (libvulkan_env) vulkan_module = dlopen(libvulkan_env, RTLD_NOW); + if (!vulkan_module) + { + unsigned path_size = 0; + _NSGetExecutablePath(nullptr, &path_size); + std::string path; + path.resize(path_size); + if (_NSGetExecutablePath(path.data(), &path_size) == 0) + { + path[path_size] = 0; + + size_t pos = path.rfind('/'); + if (pos != std::string::npos) + { + path.erase(pos); + path += "/../Frameworks/libvulkan.dylib"; + vulkan_module = dlopen(path.c_str(), RTLD_NOW); + } + } + } if (!vulkan_module) vulkan_module = dlopen("libvulkan.dylib", RTLD_NOW); #else From 04815002f0ac332e1ae75da0050d258fec893755 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 25 Aug 2020 22:03:05 +1000 Subject: [PATCH 14/61] README.md: Update Mac build instructions --- README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 394ad69f8..56e4bd7cf 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A "BIOS" ROM image is required to to start the emulator and to play games. You c ## Latest News +- 2020/08/25: Automated builds for macOS now available. - 2020/08/22: XInput controller backend added. - 2020/08/20: Per-game setting overrides added. Mostly for compatibility, but some options are customizable. - 2020/08/19: CPU PGXP mode added. It is very slow and incompatible with the recompiler, only use for games which need it. @@ -96,6 +97,20 @@ To download: - Run `chmod a+x` on the downloaded AppImage -- following this step, the AppImage can be run like a typical executable. - Optionally use a program such as [appimaged](https://github.com/AppImage/appimaged) or [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) for desktop integration. [AppImageUpdate](https://github.com/AppImage/AppImageUpdate) can be used alongside appimaged to easily update your DuckStation AppImage. +### macOS + +To download: + - Go to https://github.com/stenzek/duckstation/releases/tag/latest, and download the Mac build. This is a zip archive containing the prebuilt binary. + - Alternatively, direct download link: https://github.com/stenzek/duckstation/releases/download/latest/duckstation-mac-release.zip + - Extract the zip archive. If you're using Safari, apparently this happens automatically. This will give you DuckStation.app. + - Right click DuckStation.app, and click Open. As the package is not signed (Mac certificates are expensive), you must do this the first time you open it. Subsequent runs can be done by double-clicking. + +macOS support is considered experimental and not actively supported by the developer; the builds are provided here as a courtesy. Please feel free to submit issues, but it may be some time before +they are investigated. + +**macOS builds do not support automatic updates yet.** If there is sufficient demand, this may be something I will consider. + + ### Android A prebuilt APK is now available for Android. However, please keep in mind that the Android version is not yet feature complete, it is more of a preview of things to come. You will need a device running a 64-bit AArch64 userland (anything made in the last few years). @@ -170,12 +185,11 @@ Requirements: - Qt 5 (`brew install qt5`) 1. Clone the repository. Submodules aren't necessary, there is only one and it is only used for Windows. +2. Clone the mac externals repository (for MoltenVK): `git clone https://github.com/stenzek/duckstation-ext-mac.git dep/mac`. 2. Create a build directory, either in-tree or elsewhere, e.g. `mkdir build-release`, `cd build-release`. 3. Run cmake to configure the build system: `cmake -DCMAKE_BUILD_TYPE=Release -DQt5_DIR=/usr/local/opt/qt/lib/cmake/Qt5 ..`. You may need to tweak `Qt5_DIR` depending on your system. 4. Compile the source code: `make`. Use `make -jN` where `N` is the number of CPU cores in your system for a faster build. -5. Run the binary, located in the build directory under `bin/duckstation-sdl`, or `bin/duckstation-qt`. - -Application bundles/.apps are currently not created, so you can't launch it via Finder yet. This is planned for the future. +5. Run the binary, located in the build directory under `bin/duckstation-sdl`, or `bin/DuckStation.app` for Qt. ### Android **NOTE:** The Android frontend is still incomplete, not all functionality is available yet. User directory is hardcoded to `/sdcard/duckstation` for now. From 4391d63d0c32edd9f2c0a753d9ad3350230d6baf Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 25 Aug 2020 22:04:55 +1000 Subject: [PATCH 15/61] Build: Create macOS .app for Qt --- .gitignore | 3 + CMakeModules/DolphinPostprocessBundle.cmake | 46 +++++++++++++++ src/duckstation-qt/CMakeLists.txt | 60 ++++++++++++++++++++ src/duckstation-qt/DuckStation.icns | Bin 0 -> 259069 bytes src/duckstation-qt/Info.plist.in | 45 +++++++++++++++ src/duckstation-qt/qt.conf | 0 6 files changed, 154 insertions(+) create mode 100644 CMakeModules/DolphinPostprocessBundle.cmake create mode 100644 src/duckstation-qt/DuckStation.icns create mode 100644 src/duckstation-qt/Info.plist.in create mode 100644 src/duckstation-qt/qt.conf diff --git a/.gitignore b/.gitignore index e39a3bd3d..e49baff2e 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ CMakeLists.txt.user # python bytecode __pycache__ + +# other repos +/dep/mac diff --git a/CMakeModules/DolphinPostprocessBundle.cmake b/CMakeModules/DolphinPostprocessBundle.cmake new file mode 100644 index 000000000..ba412ba48 --- /dev/null +++ b/CMakeModules/DolphinPostprocessBundle.cmake @@ -0,0 +1,46 @@ +# This module can be used in two different ways. +# +# When invoked as `cmake -P DolphinPostprocessBundle.cmake`, it fixes up an +# application folder to be standalone. It bundles all required libraries from +# the system and fixes up library IDs. Any additional shared libraries, like +# plugins, that are found under Contents/MacOS/ will be made standalone as well. +# +# When called with `include(DolphinPostprocessBundle)`, it defines a helper +# function `dolphin_postprocess_bundle` that sets up the command form of the +# module as a post-build step. + +if(CMAKE_GENERATOR) + # Being called as include(DolphinPostprocessBundle), so define a helper function. + set(_DOLPHIN_POSTPROCESS_BUNDLE_MODULE_LOCATION "${CMAKE_CURRENT_LIST_FILE}") + function(dolphin_postprocess_bundle target) + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -DDOLPHIN_BUNDLE_PATH="$/../.." + -P "${_DOLPHIN_POSTPROCESS_BUNDLE_MODULE_LOCATION}" + ) + endfunction() + return() +endif() + +get_filename_component(DOLPHIN_BUNDLE_PATH "${DOLPHIN_BUNDLE_PATH}" ABSOLUTE) +message(STATUS "Fixing up application bundle: ${DOLPHIN_BUNDLE_PATH}") + +# Make sure to fix up any additional shared libraries (like plugins) that are +# needed. +file(GLOB_RECURSE extra_libs "${DOLPHIN_BUNDLE_PATH}/Contents/MacOS/*.dylib") + +# BundleUtilities doesn't support DYLD_FALLBACK_LIBRARY_PATH behavior, which +# makes it sometimes break on libraries that do weird things with @rpath. Specify +# equivalent search directories until https://gitlab.kitware.com/cmake/cmake/issues/16625 +# is fixed and in our minimum CMake version. +set(extra_dirs "/usr/local/lib" "/lib" "/usr/lib") + +# BundleUtilities is overly verbose, so disable most of its messages +function(message) + if(NOT ARGV MATCHES "^STATUS;") + _message(${ARGV}) + endif() +endfunction() + +include(BundleUtilities) +set(BU_CHMOD_BUNDLE_ITEMS ON) +fixup_bundle("${DOLPHIN_BUNDLE_PATH}" "${extra_libs}" "${extra_dirs}") diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 3efd34db4..5a5d186ce 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -105,3 +105,63 @@ if(WIN32) COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/qt.conf.win" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/qt.conf" ) endif() + +if(APPLE) + include(BundleUtilities) + set(BUNDLE_PATH ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/DuckStation.app) + + # Ask for an application bundle. + set_target_properties(duckstation-qt PROPERTIES + MACOSX_BUNDLE true + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in + OUTPUT_NAME DuckStation + ) + + # Copy qt.conf into the bundle + target_sources(duckstation-qt PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/qt.conf") + set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/qt.conf" PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + + # Copy icon into the bundle + target_sources(duckstation-qt PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/DuckStation.icns") + set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/DuckStation.icns" PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + + # Copy Qt plugins into the bundle + get_target_property(qtcocoa_location Qt5::QCocoaIntegrationPlugin LOCATION) + target_sources(duckstation-qt PRIVATE "${qtcocoa_location}") + set_source_files_properties("${qtcocoa_location}" PROPERTIES MACOSX_PACKAGE_LOCATION MacOS/platforms) + + get_target_property(qtmacstyle_location Qt5::QMacStylePlugin LOCATION) + target_sources(duckstation-qt PRIVATE "${qtmacstyle_location}") + set_source_files_properties("${qtmacstyle_location}" PROPERTIES MACOSX_PACKAGE_LOCATION MacOS/styles) + + # Copy resources into the bundle + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data") + file(GLOB_RECURSE resources RELATIVE "${CMAKE_SOURCE_DIR}/data" "${CMAKE_SOURCE_DIR}/data/*") + foreach(res ${resources}) + target_sources(duckstation-qt PRIVATE "${CMAKE_SOURCE_DIR}/data/${res}") + get_filename_component(resdir "${res}" DIRECTORY) + set_source_files_properties("${CMAKE_SOURCE_DIR}/data/${res}" PROPERTIES + MACOSX_PACKAGE_LOCATION "MacOS/${resdir}") + source_group("Resources" FILES "${CMAKE_SOURCE_DIR}/data/${res}") + endforeach() + + # Copy translations into the bundle + add_custom_command(TARGET duckstation-qt + POST_BUILD + COMMAND mkdir $/translations + COMMAND cp ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/translations/*.qm $/translations) + + # Copy MoltenVK into the bundle + target_sources(duckstation-qt PRIVATE "${CMAKE_SOURCE_DIR}/dep/mac/MoltenVK/libvulkan.dylib") + set_source_files_properties("${CMAKE_SOURCE_DIR}/dep/mac/MoltenVK/libvulkan.dylib" PROPERTIES MACOSX_PACKAGE_LOCATION Frameworks) + + # Update library references to make the bundle portable + include(DolphinPostprocessBundle) + dolphin_postprocess_bundle(duckstation-qt) + # Fix rpath + add_custom_command(TARGET duckstation-qt + POST_BUILD COMMAND + ${CMAKE_INSTALL_NAME_TOOL} -add_rpath "@executable_path/../Frameworks/" + $) +endif() + diff --git a/src/duckstation-qt/DuckStation.icns b/src/duckstation-qt/DuckStation.icns new file mode 100644 index 0000000000000000000000000000000000000000..fe463b16e658947ccab42474b4e59906f17deb04 GIT binary patch literal 259069 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 z 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 literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/Info.plist.in b/src/duckstation-qt/Info.plist.in new file mode 100644 index 000000000..572ee0d29 --- /dev/null +++ b/src/duckstation-qt/Info.plist.in @@ -0,0 +1,45 @@ + + + + + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + bin + cue + img + chd + m3u + psexe + psf + + CFBundleTypeIconFile + DuckStation.icns + CFBundleTypeName + PlayStation File + CFBundleTypeRole + Viewer + + + CFBundleExecutable + DuckStation + CFBundleIconFile + DuckStation.icns + CFBundleIdentifier + com.github.stenzek.duckstation + CFBundleDevelopmentRegion + English + CFBundlePackageType + APPL + NSHumanReadableCopyright + Licensed under GPL version 3 + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHighResolutionCapable + + CSResourcesFileMapped + + + diff --git a/src/duckstation-qt/qt.conf b/src/duckstation-qt/qt.conf new file mode 100644 index 000000000..e69de29bb From 69a9e5e6a93b09a6b2d92dc369785825ac0f17fb Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 25 Aug 2020 22:05:05 +1000 Subject: [PATCH 16/61] CI: Build macOS .app --- .github/workflows/rolling-release.yml | 45 ++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rolling-release.yml b/.github/workflows/rolling-release.yml index 46f815090..a7fe7af5a 100644 --- a/.github/workflows/rolling-release.yml +++ b/.github/workflows/rolling-release.yml @@ -233,9 +233,46 @@ jobs: name: "android" path: "duckstation-android-aarch64.apk" + macos-build: + runs-on: macos-10.15 + steps: + - uses: actions/checkout@v2.3.1 + with: + fetch-depth: 0 + + - name: Install packages + shell: bash + run: | + brew install qt5 sdl2 + + - name: Clone mac externals + shell: bash + run: | + git clone https://github.com/stenzek/duckstation-ext-mac.git dep/mac + + - name: Compile build + shell: bash + run: | + mkdir build + cd build + cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SDL_FRONTEND=OFF -DBUILD_QT_FRONTEND=ON -DUSE_SDL2=ON -DQt5_DIR=/usr/local/opt/qt/lib/cmake/Qt5 .. + cmake --build . --parallel 2 + + - name: Zip macOS .app + shell: bash + run: | + cd build/bin + zip -r duckstation-mac-release.zip DuckStation.app/ + + - name: Upload macOS .app + uses: actions/upload-artifact@v1 + with: + name: "macos-x64" + path: "build/bin/duckstation-mac-release.zip" + create-release: - needs: [windows-build, windows-libretro-build, linux-build, linux-libretro-build, android-build] + needs: [windows-build, windows-libretro-build, linux-build, linux-libretro-build, android-build, macos-build] runs-on: "ubuntu-latest" if: github.ref == 'refs/heads/master' steps: @@ -279,6 +316,11 @@ jobs: with: name: "android" + - name: Download Mac App + uses: actions/download-artifact@v1 + with: + name: "macos-x64" + - name: Create release uses: "marvinpinto/action-automatic-releases@latest" with: @@ -296,4 +338,5 @@ jobs: linux-libretro/duckstation_libretro_linux_aarch64.so.zip linux-libretro/duckstation_libretro_android_aarch64.so.zip android/duckstation-android-aarch64.apk + macos-x64/duckstation-mac-release.zip From 79aaf908a602e03a16698976791c82baafa794d0 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Wed, 26 Aug 2020 00:20:09 +1000 Subject: [PATCH 17/61] Update compatibility list --- CONTRIBUTORS.md | 2 + data/database/compatibility.xml | 97 +++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b7fb79e4f..9948ecc51 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -29,6 +29,8 @@ The following people have contributed to the project in some way, and are credit - @heckez-sys - @Damaniel - @RaydenX93 + - @gp2man + - @Richard-L ## Special Thanks The following people did not directly contribute to the emulator, but it would not be in the state if not for them. diff --git a/data/database/compatibility.xml b/data/database/compatibility.xml index 110bba0e5..8a0b9d91a 100644 --- a/data/database/compatibility.xml +++ b/data/database/compatibility.xml @@ -215,6 +215,10 @@ No Issues 0.1-1391-g5f9481dd + + No Issues + 0.1-1558-gf852be74 + No Issues 0.1-1323-ga6acd33 @@ -259,6 +263,10 @@ No Issues 0.1-1336-gd711baa + + No Issues + 0.1-1580-g136a9d60 + No Issues 0.1-1304-gc8b6712 @@ -474,6 +482,10 @@ No Issues 0.1-1304-gc8b6712 + + No Issues + 0.1-1558-gf852be74 + No Issues 0.1-986-gfc911de1 @@ -717,10 +729,10 @@ No Issues 0.1-1490-g76978986 - - Crashes In-Game - 0.1-1409-ge198e315 - Graphical errors in ingame menu. If you try to check the status, the game freezes in black screen (Issues #533 and #503). + + No Issues + 0.1-1600-g032127a7 + No Issues @@ -734,6 +746,10 @@ No Issues 0.1-1337-gcaf9943 + + No Issues + 0.1-1558-gf852be74 + No Issues 0.1-1400-gb527118c @@ -839,6 +855,11 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1425-g05f0ce6d + + Speed Issues + 0.1-1558-gf852be74 + Speed issues ingame (Issue #695). + No Issues 0.1-1308-g622e50fa @@ -910,6 +931,14 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-908-g9f22684 + + No Issues + 0.1-1558-gf852be74 + + + No Issues + 0.1-1558-gf852be74 + No Issues 0.1-1333-g5a955a4 @@ -922,6 +951,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1333-g5a955a4 + + No Issues + 0.1-1558-gf852be74 + No Issues 0.1-1333-g5a955a4 @@ -955,6 +988,10 @@ Tetris with Card Captor Sakura (Japan) 0.1-908-g9f22684 Broken when upscaling + + No Issues + 0.1-1580-g136a9d60 + No Issues 0.1-1336-gd711baa @@ -1068,6 +1105,10 @@ Tetris with Card Captor Sakura (Japan) 0.1-986-gfc911de1 Rendering is broken with any upscaling. + + No Issues + 0.1-1580-g136a9d60 + No Issues 0.1-1308-g622e50fa @@ -1140,8 +1181,17 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-986-gfc911de1 + + Graphical/Audio Issues + 0.1-1580-g136a9d60 + Some graphics issues in random races + + + Crashes In-Game + Crashes In Intro + 0.1-1580-g136a9d60 Blackscreen after the first Loading screen (Issue #54). @@ -1171,6 +1221,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1425-g05f0ce6d + + Doesn't Boot + 0.1-1580-g136a9d60 + No Issues @@ -1322,6 +1376,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-887-g1eecd50 + + No Issues + 0.1-1580-g136a9d60 + No Issues 0.1-986-gfc911de1 @@ -2075,6 +2133,24 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1530-g6d75f42e + + Graphical/Audio Issues + 0.1-1539-gf704cc64 + In Battle Arena Toshinden demo, game runs at a strange fast speed than it should be (Issue #695). + + + No Issues + 0.1-1539-gf704cc64 + Can hang if speed up in Star Wars Rebel Assaut 2 demo. + + + No Issues + 0.1-1539-gf704cc64 + + + No Issues + 0.1-1539-gf704cc64 + No Issues 0.1-884-g096ed21 @@ -2104,6 +2180,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-774-g5a1b008 + + No Issues + 0.1-1539-gf704cc64 + No Issues 0.1-1529-ga895c027 @@ -2834,6 +2914,11 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1265-gdd9a419 + + Graphical/Audio Issues + 0.1-1558-gf852be74 + Game needs forced 60hz timing to run at correct speed, but rendered cutscenes desync as a consequence. + No Issues 0.1-986-gfc911de1 @@ -2850,6 +2935,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-986-gfc911de1 + + No Issues + 0.1-1580-g136a9d60 + No Issues 0.1-986-gfc911de1 From 78dbb4893dfa0143f9084a07f2e40a248071cf30 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Thu, 27 Aug 2020 21:20:16 +1000 Subject: [PATCH 18/61] appveyor.yml: Disable everything except Windows builds Nobody should be using these anymore, but just in case, it'll make it complete a bit quicker. --- appveyor.yml | 58 ---------------------------------------------------- 1 file changed, 58 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index e88522645..85045f701 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,27 +6,11 @@ skip_tags: true image: - Visual Studio 2019 -- Ubuntu1804 -- macOS install: - cmd: >- git submodule update --init --depth 1 -- sh: >- - if [ "$APPVEYOR_BUILD_WORKER_IMAGE" == "Ubuntu1804" ]; then - - sudo apt-get update - - sudo apt-get install -y cmake ninja-build ccache libsdl2-dev libgtk2.0-dev qtbase5-dev qtbase5-dev-tools qtbase5-private-dev qt5-default - - elif [ "$APPVEYOR_BUILD_WORKER_IMAGE" == "macOS" ]; then - - brew install qt5 sdl2 - - fi - - build_script: - cmd: >- call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 @@ -44,46 +28,4 @@ build_script: appveyor PushArtifact duckstation-win64-release.7z -- sh: >- - if [ "$APPVEYOR_BUILD_WORKER_IMAGE" == "Ubuntu1804" ]; then - - mkdir -p build-release - - cd build-release - - cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SDL_FRONTEND=ON -DBUILD_QT_FRONTEND=ON -DUSE_SDL2=ON -G Ninja .. - - ninja - - ../appimage/generate-appimages.sh $(pwd) - - if [ $? -eq 0 ]; then - - mv duckstation-qt-x64.AppImage duckstation-qt-x64-release.AppImage - - mv duckstation-sdl-x64.AppImage duckstation-sdl-x64-release.AppImage - - 7za a -r duckstation-linux-x64-release.7z duckstation-*.AppImage - - appveyor PushArtifact duckstation-linux-x64-release.7z - - else - - echo "Failed to create AppImages, no AppImage artifact will be pushed" - - fi - - elif [ "$APPVEYOR_BUILD_WORKER_IMAGE" == "macOS" ]; then - - mkdir build-release - - cd build-release - - cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_SDL_FRONTEND=YES -DBUILD_QT_FRONTEND=YES -DQt5_DIR=/usr/local/opt/qt/lib/cmake/Qt5 .. - - make - - fi - - test: off From ca723d699b01f880a75f769392bc541aa7fa5f0a Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Fri, 28 Aug 2020 21:22:14 +1000 Subject: [PATCH 19/61] Make ALWAYS_INLINE_RELEASE specify inline in debug too Stops us needing the static qualifier as well. --- src/common/types.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/types.h b/src/common/types.h index 6b94fd87f..839b585e7 100644 --- a/src/common/types.h +++ b/src/common/types.h @@ -17,7 +17,7 @@ // Force inline in non-debug helper #ifdef _DEBUG -#define ALWAYS_INLINE_RELEASE +#define ALWAYS_INLINE_RELEASE inline #else #define ALWAYS_INLINE_RELEASE ALWAYS_INLINE #endif From 3aecf6be27d9cecc8b4082f74466e617bd1a1f95 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 29 Aug 2020 21:53:52 +1000 Subject: [PATCH 20/61] GameSettings: Rename 'enable' options to 'force' As per discussion on Discord. --- data/database/gamesettings.ini | 24 ++++++++++++------------ src/core/game_settings.cpp | 28 ++++++++++++++-------------- src/core/game_settings.h | 8 ++++---- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/data/database/gamesettings.ini b/data/database/gamesettings.ini index 6b2a02c99..ccea6b7a7 100644 --- a/data/database/gamesettings.ini +++ b/data/database/gamesettings.ini @@ -3,12 +3,12 @@ # Croc - Legend of the Gobbos (USA) (SLUS-00530) [SLUS-00530] -EnablePGXPCPUMode = true +ForcePGXPCPUMode = true # Croc 2 (USA) (SLUS-00634) [SLUS-00634] -EnablePGXPCPUMode = true +ForcePGXPCPUMode = true # Doom (USA) (Rev 1) (SLUS-00077) @@ -18,54 +18,54 @@ DisableUpscaling = true # Pop'n Music 6 (Japan) (SLPM-87089) [SLPM-87089] -EnableInterlacing = true +ForceInterlacing = true # Mr. Driller G (Japan) (SLPS-03336) [SLPS-03336] -EnableInterlacing = true +ForceInterlacing = true # Pro Pinball - Big Race USA (USA) (SLUS-01260) [SLUS-01260] ForceSoftwareRenderer = true -EnableInterlacing = true +ForceInterlacing = true # Pro Pinball - Fantastic Journey (USA) (SLUS-01261) [SLUS-01261] ForceSoftwareRenderer = true -EnableInterlacing = true +ForceInterlacing = true # True Pinball (USA) (SLUS-00337) [SLUS-00337] -EnableInterlacing = true +ForceInterlacing = true # Dead or Alive (USA) (SLUS-00606) [SLUS-00606] -EnableInterlacing = true +ForceInterlacing = true # Shinobi no Sato no Jintori Gassen (Japan) (SLPS-03553) [SLPS-03553] -EnableInterlacing = true +ForceInterlacing = true # Time Bokan Series: Bokan desu yo (SLPS-01211) [SLPS-01211] -EnableInterlacing = true +ForceInterlacing = true # Rat Attack! (USA) (SLUS-00656) [SLUS-00656] -EnableInterlacing = true +ForceInterlacing = true # Arcade Party Pak (USA) (SLUS-00952) [SLUS-00952] -EnableInterlacing = true +ForceInterlacing = true # SLUS-01222 (Colin McRae Rally 2.0 (USA) (En,Fr,Es)) diff --git a/src/core/game_settings.cpp b/src/core/game_settings.cpp index 0db275879..4ad913d22 100644 --- a/src/core/game_settings.cpp +++ b/src/core/game_settings.cpp @@ -21,17 +21,17 @@ namespace GameSettings { std::array, static_cast(Trait::Count)> s_trait_names = {{ {"ForceInterpreter", TRANSLATABLE("GameSettingsTrait", "Force Interpreter")}, {"ForceSoftwareRenderer", TRANSLATABLE("GameSettingsTrait", "Force Software Renderer")}, - {"EnableInterlacing", TRANSLATABLE("GameSettingsTrait", "Enable Interlacing")}, + {"ForceInterlacing", TRANSLATABLE("GameSettingsTrait", "Force Interlacing")}, {"DisableTrueColor", TRANSLATABLE("GameSettingsTrait", "Disable True Color")}, {"DisableUpscaling", TRANSLATABLE("GameSettingsTrait", "Disable Upscaling")}, {"DisableScaledDithering", TRANSLATABLE("GameSettingsTrait", "Disable Scaled Dithering")}, {"DisableWidescreen", TRANSLATABLE("GameSettingsTrait", "Disable Widescreen")}, {"DisablePGXP", TRANSLATABLE("GameSettingsTrait", "Disable PGXP")}, {"DisablePGXPCulling", TRANSLATABLE("GameSettingsTrait", "Disable PGXP Culling")}, - {"EnablePGXPVertexCache", TRANSLATABLE("GameSettingsTrait", "Enable PGXP Vertex Cache")}, - {"EnablePGXPCPUMode", TRANSLATABLE("GameSettingsTrait", "Enable PGXP CPU Mode")}, + {"ForcePGXPVertexCache", TRANSLATABLE("GameSettingsTrait", "Force PGXP Vertex Cache")}, + {"ForcePGXPCPUMode", TRANSLATABLE("GameSettingsTrait", "Force PGXP CPU Mode")}, {"ForceDigitalController", TRANSLATABLE("GameSettingsTrait", "Force Digital Controller")}, - {"EnableRecompilerMemoryExceptions", TRANSLATABLE("GameSettingsTrait", "Enable Recompiler Memory Exceptions")}, + {"ForceRecompilerMemoryExceptions", TRANSLATABLE("GameSettingsTrait", "Force Recompiler Memory Exceptions")}, }}; const char* GetTraitName(Trait trait) @@ -305,7 +305,7 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::ForceInterpreter)) { if (display_osd_messages && g_settings.cpu_execution_mode != CPUExecutionMode::Interpreter) - g_host_interface->AddOSDMessage("CPU execution mode forced to interpreter by game settings.", osd_duration); + g_host_interface->AddOSDMessage("CPU interpreter forced by game settings.", osd_duration); g_settings.cpu_execution_mode = CPUExecutionMode::Interpreter; } @@ -313,15 +313,15 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::ForceSoftwareRenderer)) { if (display_osd_messages && g_settings.gpu_renderer != GPURenderer::Software) - g_host_interface->AddOSDMessage("GPU renderer forced to software by game settings.", osd_duration); + g_host_interface->AddOSDMessage("Software renderer forced by game settings.", osd_duration); g_settings.gpu_renderer = GPURenderer::Software; } - if (HasTrait(Trait::EnableInterlacing)) + if (HasTrait(Trait::ForceInterlacing)) { if (display_osd_messages && g_settings.gpu_disable_interlacing) - g_host_interface->AddOSDMessage("Interlacing enabled by game settings.", osd_duration); + g_host_interface->AddOSDMessage("Interlacing forced by game settings.", osd_duration); g_settings.gpu_disable_interlacing = false; } @@ -378,18 +378,18 @@ void Entry::ApplySettings(bool display_osd_messages) const g_settings.gpu_pgxp_culling = false; } - if (HasTrait(Trait::EnablePGXPVertexCache)) + if (HasTrait(Trait::ForcePGXPVertexCache)) { if (display_osd_messages && g_settings.gpu_pgxp_enable && !g_settings.gpu_pgxp_vertex_cache) - g_host_interface->AddOSDMessage("PGXP vertex cache enabled by game settings.", osd_duration); + g_host_interface->AddOSDMessage("PGXP vertex cache forced by game settings.", osd_duration); g_settings.gpu_pgxp_vertex_cache = true; } - if (HasTrait(Trait::EnablePGXPCPUMode)) + if (HasTrait(Trait::ForcePGXPCPUMode)) { if (display_osd_messages && g_settings.gpu_pgxp_enable && !g_settings.gpu_pgxp_cpu) - g_host_interface->AddOSDMessage("PGXP CPU mode enabled by game settings.", osd_duration); + g_host_interface->AddOSDMessage("PGXP CPU mode forced by game settings.", osd_duration); g_settings.gpu_pgxp_cpu = true; } @@ -412,12 +412,12 @@ void Entry::ApplySettings(bool display_osd_messages) const } } - if (HasTrait(Trait::EnableRecompilerMemoryExceptions)) + if (HasTrait(Trait::ForceRecompilerMemoryExceptions)) { if (display_osd_messages && g_settings.cpu_execution_mode == CPUExecutionMode::Recompiler && !g_settings.cpu_recompiler_memory_exceptions) { - g_host_interface->AddOSDMessage("Recompiler memory exceptions enabled by game settings.", osd_duration); + g_host_interface->AddOSDMessage("Recompiler memory exceptions forced by game settings.", osd_duration); } g_settings.cpu_recompiler_memory_exceptions = true; diff --git a/src/core/game_settings.h b/src/core/game_settings.h index 09907dd79..45b8bd0ac 100644 --- a/src/core/game_settings.h +++ b/src/core/game_settings.h @@ -12,17 +12,17 @@ enum class Trait : u32 { ForceInterpreter, ForceSoftwareRenderer, - EnableInterlacing, + ForceInterlacing, DisableTrueColor, DisableUpscaling, DisableScaledDithering, DisableWidescreen, DisablePGXP, DisablePGXPCulling, - EnablePGXPVertexCache, - EnablePGXPCPUMode, + ForcePGXPVertexCache, + ForcePGXPCPUMode, ForceDigitalController, - EnableRecompilerMemoryExceptions, + ForceRecompilerMemoryExceptions, Count }; From efc00a2d0ea4174ebd79dababa6d43e66e440830 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 29 Aug 2020 21:57:29 +1000 Subject: [PATCH 21/61] GameSettings: Make override messages translatable --- src/core/game_settings.cpp | 67 +++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/src/core/game_settings.cpp b/src/core/game_settings.cpp index 4ad913d22..1d84d5943 100644 --- a/src/core/game_settings.cpp +++ b/src/core/game_settings.cpp @@ -305,7 +305,10 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::ForceInterpreter)) { if (display_osd_messages && g_settings.cpu_execution_mode != CPUExecutionMode::Interpreter) - g_host_interface->AddOSDMessage("CPU interpreter forced by game settings.", osd_duration); + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "CPU interpreter forced by game settings."), osd_duration); + } g_settings.cpu_execution_mode = CPUExecutionMode::Interpreter; } @@ -313,7 +316,10 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::ForceSoftwareRenderer)) { if (display_osd_messages && g_settings.gpu_renderer != GPURenderer::Software) - g_host_interface->AddOSDMessage("Software renderer forced by game settings.", osd_duration); + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "Software renderer forced by game settings."), osd_duration); + } g_settings.gpu_renderer = GPURenderer::Software; } @@ -321,7 +327,10 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::ForceInterlacing)) { if (display_osd_messages && g_settings.gpu_disable_interlacing) - g_host_interface->AddOSDMessage("Interlacing forced by game settings.", osd_duration); + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "Interlacing forced by game settings."), osd_duration); + } g_settings.gpu_disable_interlacing = false; } @@ -329,7 +338,10 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::DisableTrueColor)) { if (display_osd_messages && g_settings.gpu_true_color) - g_host_interface->AddOSDMessage("True color disabled by game settings.", osd_duration); + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "True color disabled by game settings."), osd_duration); + } g_settings.gpu_true_color = false; } @@ -337,7 +349,10 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::DisableUpscaling)) { if (display_osd_messages && g_settings.gpu_resolution_scale > 1) - g_host_interface->AddOSDMessage("Upscaling disabled by game settings.", osd_duration); + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "Upscaling disabled by game settings."), osd_duration); + } g_settings.gpu_resolution_scale = 1; } @@ -345,7 +360,11 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::DisableScaledDithering)) { if (display_osd_messages && g_settings.gpu_scaled_dithering) - g_host_interface->AddOSDMessage("Scaled dithering disabled by game settings.", osd_duration); + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "Scaled dithering disabled by game settings."), + osd_duration); + } g_settings.gpu_scaled_dithering = false; } @@ -355,7 +374,8 @@ void Entry::ApplySettings(bool display_osd_messages) const if (display_osd_messages && (g_settings.display_aspect_ratio == DisplayAspectRatio::R16_9 || g_settings.gpu_widescreen_hack)) { - g_host_interface->AddOSDMessage("Widescreen disabled by game settings.", osd_duration); + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "Widescreen disabled by game settings."), osd_duration); } g_settings.display_aspect_ratio = DisplayAspectRatio::R4_3; @@ -365,7 +385,11 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::DisablePGXP)) { if (display_osd_messages && g_settings.gpu_pgxp_enable) - g_host_interface->AddOSDMessage("PGXP geometry correction disabled by game settings.", osd_duration); + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "PGXP geometry correction disabled by game settings."), + osd_duration); + } g_settings.gpu_pgxp_enable = false; } @@ -373,7 +397,10 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::DisablePGXPCulling)) { if (display_osd_messages && g_settings.gpu_pgxp_culling) - g_host_interface->AddOSDMessage("PGXP culling disabled by game settings.", osd_duration); + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "PGXP culling disabled by game settings."), osd_duration); + } g_settings.gpu_pgxp_culling = false; } @@ -381,7 +408,10 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::ForcePGXPVertexCache)) { if (display_osd_messages && g_settings.gpu_pgxp_enable && !g_settings.gpu_pgxp_vertex_cache) - g_host_interface->AddOSDMessage("PGXP vertex cache forced by game settings.", osd_duration); + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "PGXP vertex cache forced by game settings."), osd_duration); + } g_settings.gpu_pgxp_vertex_cache = true; } @@ -389,7 +419,10 @@ void Entry::ApplySettings(bool display_osd_messages) const if (HasTrait(Trait::ForcePGXPCPUMode)) { if (display_osd_messages && g_settings.gpu_pgxp_enable && !g_settings.gpu_pgxp_cpu) - g_host_interface->AddOSDMessage("PGXP CPU mode forced by game settings.", osd_duration); + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "PGXP CPU mode forced by game settings."), osd_duration); + } g_settings.gpu_pgxp_cpu = true; } @@ -403,8 +436,10 @@ void Entry::ApplySettings(bool display_osd_messages) const { if (display_osd_messages) { - g_host_interface->AddFormattedOSDMessage(osd_duration, "Controller %u changed to digital by game settings.", - i + 1u); + g_host_interface->AddFormattedOSDMessage( + osd_duration, + g_host_interface->TranslateString("OSDMessage", "Controller %u changed to digital by game settings."), + i + 1u); } g_settings.controller_types[i] = ControllerType::DigitalController; @@ -417,13 +452,13 @@ void Entry::ApplySettings(bool display_osd_messages) const if (display_osd_messages && g_settings.cpu_execution_mode == CPUExecutionMode::Recompiler && !g_settings.cpu_recompiler_memory_exceptions) { - g_host_interface->AddOSDMessage("Recompiler memory exceptions forced by game settings.", osd_duration); + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "Recompiler memory exceptions forced by game settings."), + osd_duration); } g_settings.cpu_recompiler_memory_exceptions = true; } - - // TODO: Overscan settings. } } // namespace GameSettings \ No newline at end of file From 19d6037b9993acfa8817516bc6e68ae721aae27a Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 29 Aug 2020 22:07:33 +1000 Subject: [PATCH 22/61] CPU: Implement instruction cache simulation Implemented for all execution modes. Disabled by default in the cached interpreter and recompiler, always enabled in the pure interpreter. --- src/core/bus.cpp | 229 ++++++++++++++++-- src/core/bus.h | 35 --- src/core/cpu_code_cache.cpp | 33 ++- src/core/cpu_code_cache.h | 2 + src/core/cpu_core.cpp | 8 +- src/core/cpu_core.h | 32 ++- src/core/cpu_core_private.h | 30 +++ src/core/cpu_recompiler_code_generator.cpp | 5 +- src/core/cpu_recompiler_code_generator.h | 1 + .../cpu_recompiler_code_generator_generic.cpp | 44 ++++ .../cpu_recompiler_code_generator_x64.cpp | 46 ++++ src/core/cpu_recompiler_thunks.h | 1 + src/core/host_interface.cpp | 15 +- src/core/settings.cpp | 2 + src/core/settings.h | 1 + .../libretro_host_interface.cpp | 8 +- src/duckstation-qt/advancedsettingswidget.cpp | 6 + src/duckstation-qt/advancedsettingswidget.ui | 21 +- src/duckstation-sdl/sdl_host_interface.cpp | 5 + 19 files changed, 449 insertions(+), 75 deletions(-) diff --git a/src/core/bus.cpp b/src/core/bus.cpp index e6fa98296..7b8a70c0a 100644 --- a/src/core/bus.cpp +++ b/src/core/bus.cpp @@ -742,10 +742,153 @@ ALWAYS_INLINE static TickCount DoDMAAccess(u32 offset, u32& value) namespace CPU { +template +ALWAYS_INLINE_RELEASE void DoInstructionRead(PhysicalMemoryAddress address, void* data) +{ + using namespace Bus; + + address &= PHYSICAL_MEMORY_ADDRESS_MASK; + + if (address < RAM_MIRROR_END) + { + std::memcpy(data, &g_ram[address & RAM_MASK], sizeof(u32) * word_count); + if constexpr (add_ticks) + g_state.pending_ticks += (icache_read ? 1 : 4) * word_count; + } + else if (address >= BIOS_BASE && address < (BIOS_BASE + BIOS_SIZE)) + { + std::memcpy(data, &g_bios[(address - BIOS_BASE) & BIOS_MASK], sizeof(u32)); + if constexpr (add_ticks) + g_state.pending_ticks += m_bios_access_time[static_cast(MemoryAccessSize::Word)] * word_count; + } + else + { + CPU::RaiseException(address, Cop0Registers::CAUSE::MakeValueForException(Exception::IBE, false, false, 0)); + std::memset(data, 0, sizeof(u32) * word_count); + } +} + +TickCount GetInstructionReadTicks(VirtualMemoryAddress address) +{ + using namespace Bus; + + address &= PHYSICAL_MEMORY_ADDRESS_MASK; + + if (address < RAM_MIRROR_END) + { + return 4; + } + else if (address >= BIOS_BASE && address < (BIOS_BASE + BIOS_SIZE)) + { + return m_bios_access_time[static_cast(MemoryAccessSize::Word)]; + } + else + { + return 0; + } +} + +TickCount GetICacheFillTicks(VirtualMemoryAddress address) +{ + using namespace Bus; + + address &= PHYSICAL_MEMORY_ADDRESS_MASK; + + if (address < RAM_MIRROR_END) + { + return 1 * (ICACHE_LINE_SIZE / sizeof(u32)); + } + else if (address >= BIOS_BASE && address < (BIOS_BASE + BIOS_SIZE)) + { + return m_bios_access_time[static_cast(MemoryAccessSize::Word)] * (ICACHE_LINE_SIZE / sizeof(u32)); + } + else + { + return 0; + } +} + +void CheckAndUpdateICacheTags(u32 line_count, TickCount uncached_ticks) +{ + VirtualMemoryAddress current_pc = g_state.regs.pc & ICACHE_TAG_ADDRESS_MASK; + if (IsCachedAddress(current_pc)) + { + TickCount ticks = 0; + TickCount cached_ticks_per_line = GetICacheFillTicks(current_pc); + for (u32 i = 0; i < line_count; i++, current_pc += ICACHE_LINE_SIZE) + { + const u32 line = GetICacheLine(current_pc); + if (g_state.icache_tags[line] != current_pc) + { + g_state.icache_tags[line] = current_pc; + ticks += cached_ticks_per_line; + } + } + + g_state.pending_ticks += ticks; + } + else + { + g_state.pending_ticks += uncached_ticks; + } +} + +u32 FillICache(VirtualMemoryAddress address) +{ + const u32 line = GetICacheLine(address); + g_state.icache_tags[line] = GetICacheTagForAddress(address); + u8* line_data = &g_state.icache_data[line * ICACHE_LINE_SIZE]; + DoInstructionRead(address & ~(ICACHE_LINE_SIZE - 1u), line_data); + + const u32 offset = GetICacheLineOffset(address); + u32 result; + std::memcpy(&result, &line_data[offset], sizeof(result)); + return result; +} + +void ClearICache() +{ + std::memset(g_state.icache_data.data(), 0, ICACHE_SIZE); + g_state.icache_tags.fill(ICACHE_INVALD_BIT | ICACHE_DISABLED_BIT); +} + +ALWAYS_INLINE_RELEASE static u32 ReadICache(VirtualMemoryAddress address) +{ + const u32 line = GetICacheLine(address); + const u8* line_data = &g_state.icache_data[line * ICACHE_LINE_SIZE]; + const u32 offset = GetICacheLineOffset(address); + u32 result; + std::memcpy(&result, &line_data[offset], sizeof(result)); + return result; +} + +ALWAYS_INLINE_RELEASE static void WriteICache(VirtualMemoryAddress address, u32 value) +{ + const u32 line = GetICacheLine(address); + const u32 offset = GetICacheLineOffset(address); + g_state.icache_tags[line] = GetICacheTagForAddress(address) | ICACHE_INVALD_BIT; + std::memcpy(&g_state.icache_data[line * ICACHE_LINE_SIZE + offset], &value, sizeof(value)); +} + static void WriteCacheControl(u32 value) { Log_WarningPrintf("Cache control <- 0x%08X", value); - g_state.cache_control = value; + + CacheControl changed_bits{g_state.cache_control.bits ^ value}; + g_state.cache_control.bits = value; + if (changed_bits.icache_enable) + { + if (g_state.cache_control.icache_enable) + { + for (u32 i = 0; i < ICACHE_LINES; i++) + g_state.icache_tags[i] &= ~ICACHE_DISABLED_BIT; + } + else + { + for (u32 i = 0; i < ICACHE_LINES; i++) + g_state.icache_tags[i] |= ICACHE_DISABLED_BIT; + } + } } template @@ -797,7 +940,10 @@ static ALWAYS_INLINE TickCount DoMemoryAccess(VirtualMemoryAddress address, u32& if constexpr (type == MemoryAccessType::Write) { if (g_state.cop0_regs.sr.Isc) + { + WriteICache(address, value); return 0; + } } address &= PHYSICAL_MEMORY_ADDRESS_MASK; @@ -829,7 +975,7 @@ static ALWAYS_INLINE TickCount DoMemoryAccess(VirtualMemoryAddress address, u32& if (address == 0xFFFE0130) { if constexpr (type == MemoryAccessType::Read) - value = g_state.cache_control; + value = g_state.cache_control.bits; else WriteCacheControl(value); @@ -849,6 +995,10 @@ static ALWAYS_INLINE TickCount DoMemoryAccess(VirtualMemoryAddress address, u32& { return DoRAMAccess(address, value); } + else if (address >= BIOS_BASE && address < (BIOS_BASE + BIOS_SIZE)) + { + return DoBIOSAccess(static_cast(address - BIOS_BASE), value); + } else if (address < EXP1_BASE) { return DoInvalidAccess(type, size, address, value); @@ -921,14 +1071,6 @@ static ALWAYS_INLINE TickCount DoMemoryAccess(VirtualMemoryAddress address, u32& { return DoEXP2Access(address & EXP2_MASK, value); } - else if (address < BIOS_BASE) - { - return DoInvalidAccess(type, size, address, value); - } - else if (address < (BIOS_BASE + BIOS_SIZE)) - { - return DoBIOSAccess(static_cast(address - BIOS_BASE), value); - } else { return DoInvalidAccess(type, size, address, value); @@ -961,12 +1103,45 @@ static bool DoAlignmentCheck(VirtualMemoryAddress address) bool FetchInstruction() { DebugAssert(Common::IsAlignedPow2(g_state.regs.npc, 4)); - if (DoMemoryAccess(g_state.regs.npc, g_state.next_instruction.bits) < - 0) + + using namespace Bus; + + PhysicalMemoryAddress address = g_state.regs.npc; + switch (address >> 29) { - // Bus errors don't set BadVaddr. - RaiseException(g_state.regs.npc, Cop0Registers::CAUSE::MakeValueForException(Exception::IBE, false, false, 0)); - return false; + case 0x00: // KUSEG 0M-512M + case 0x04: // KSEG0 - physical memory cached + { +#if 0 + // TODO: icache + TickCount cycles; + DoInstructionRead(address, cycles, g_state.next_instruction.bits); +#else + if (CompareICacheTag(address)) + g_state.next_instruction.bits = ReadICache(address); + else + g_state.next_instruction.bits = FillICache(address); + +#endif + } + break; + + case 0x05: // KSEG1 - physical memory uncached + { + DoInstructionRead(address, &g_state.next_instruction.bits); + } + break; + + case 0x01: // KUSEG 512M-1024M + case 0x02: // KUSEG 1024M-1536M + case 0x03: // KUSEG 1536M-2048M + case 0x06: // KSEG2 + case 0x07: // KSEG2 + default: + { + CPU::RaiseException(address, Cop0Registers::CAUSE::MakeValueForException(Exception::IBE, false, false, 0)); + return false; + } } g_state.regs.pc = g_state.regs.npc; @@ -974,6 +1149,30 @@ bool FetchInstruction() return true; } +bool SafeReadInstruction(VirtualMemoryAddress addr, u32* value) +{ + switch (addr >> 29) + { + case 0x00: // KUSEG 0M-512M + case 0x04: // KSEG0 - physical memory cached + case 0x05: // KSEG1 - physical memory uncached + { + DoInstructionRead(addr, value); + return true; + } + + case 0x01: // KUSEG 512M-1024M + case 0x02: // KUSEG 1024M-1536M + case 0x03: // KUSEG 1536M-2048M + case 0x06: // KSEG2 + case 0x07: // KSEG2 + default: + { + return false; + } + } +} + bool ReadMemoryByte(VirtualMemoryAddress addr, u8* value) { u32 temp = 0; diff --git a/src/core/bus.h b/src/core/bus.h index a18ba1274..10c44f90e 100644 --- a/src/core/bus.h +++ b/src/core/bus.h @@ -78,41 +78,6 @@ extern std::bitset m_ram_code_bits; extern u8 g_ram[RAM_SIZE]; // 2MB RAM extern u8 g_bios[BIOS_SIZE]; // 512K BIOS ROM -/// Returns the address which should be used for code caching (i.e. removes mirrors). -ALWAYS_INLINE PhysicalMemoryAddress UnmirrorAddress(PhysicalMemoryAddress address) -{ - // RAM - if (address < 0x800000) - return address & UINT32_C(0x1FFFFF); - else - return address; -} - -/// Returns true if the address specified is cacheable (RAM or BIOS). -ALWAYS_INLINE bool IsCacheableAddress(PhysicalMemoryAddress address) -{ - return (address < RAM_MIRROR_END) || (address >= BIOS_BASE && address < (BIOS_BASE + BIOS_SIZE)); -} - -/// Reads a cachable address (RAM or BIOS). -ALWAYS_INLINE u32 ReadCacheableAddress(PhysicalMemoryAddress address) -{ - u32 value; - if (address < RAM_MIRROR_END) - { - std::memcpy(&value, &g_ram[address & RAM_MASK], sizeof(value)); - return value; - } - else - { - std::memcpy(&value, &g_bios[address & BIOS_MASK], sizeof(value)); - return value; - } -} - -/// Returns true if the address specified is writable (RAM). -ALWAYS_INLINE bool IsRAMAddress(PhysicalMemoryAddress address) { return address < RAM_MIRROR_END; } - /// Flags a RAM region as code, so we know when to invalidate blocks. ALWAYS_INLINE void SetRAMCodePage(u32 index) { m_ram_code_bits[index] = true; } diff --git a/src/core/cpu_code_cache.cpp b/src/core/cpu_code_cache.cpp index b4fc78ebb..727f449d8 100644 --- a/src/core/cpu_code_cache.cpp +++ b/src/core/cpu_code_cache.cpp @@ -139,8 +139,7 @@ static void ExecuteImpl() { if (HasPendingInterrupt()) { - // TODO: Fill in m_next_instruction... - SafeReadMemoryWord(g_state.regs.pc, &g_state.next_instruction.bits); + SafeReadInstruction(g_state.regs.pc, &g_state.next_instruction.bits); DispatchInterrupt(); next_block_key = GetNextBlockKey(); } @@ -165,6 +164,9 @@ static void ExecuteImpl() LogCurrentState(); #endif + if (g_settings.cpu_recompiler_icache) + CheckAndUpdateICacheTags(block->icache_line_count, block->uncached_fetch_ticks); + InterpretCachedBlock(*block); if (g_state.pending_ticks >= g_state.downcount) @@ -247,7 +249,7 @@ void ExecuteRecompiler() { if (HasPendingInterrupt()) { - SafeReadMemoryWord(g_state.regs.pc, &g_state.next_instruction.bits); + SafeReadInstruction(g_state.regs.pc, &g_state.next_instruction.bits); DispatchInterrupt(); } @@ -351,7 +353,8 @@ bool RevalidateBlock(CodeBlock* block) { for (const CodeBlockInstruction& cbi : block->instructions) { - u32 new_code = Bus::ReadCacheableAddress(cbi.pc & PHYSICAL_MEMORY_ADDRESS_MASK); + u32 new_code = 0; + SafeReadInstruction(cbi.pc, &new_code); if (cbi.instruction.bits != new_code) { Log_DebugPrintf("Block 0x%08X changed at PC 0x%08X - %08X to %08X - recompiling.", block->GetPC(), cbi.pc, @@ -395,16 +398,12 @@ bool CompileBlock(CodeBlock* block) __debugbreak(); #endif + u32 last_cache_line = ICACHE_LINES; + for (;;) { CodeBlockInstruction cbi = {}; - - const PhysicalMemoryAddress phys_addr = pc & PHYSICAL_MEMORY_ADDRESS_MASK; - if (!Bus::IsCacheableAddress(phys_addr)) - break; - - cbi.instruction.bits = Bus::ReadCacheableAddress(phys_addr); - if (!IsInvalidInstruction(cbi.instruction)) + if (!SafeReadInstruction(pc, &cbi.instruction.bits) || !IsInvalidInstruction(cbi.instruction)) break; cbi.pc = pc; @@ -416,6 +415,18 @@ bool CompileBlock(CodeBlock* block) cbi.has_load_delay = InstructionHasLoadDelay(cbi.instruction); cbi.can_trap = CanInstructionTrap(cbi.instruction, InUserMode()); + if (g_settings.cpu_recompiler_icache) + { + const u32 icache_line = GetICacheLine(pc); + if (icache_line != last_cache_line) + { + block->icache_line_count++; + block->icache_line_count = GetICacheFillTicks(pc); + last_cache_line = icache_line; + } + block->uncached_fetch_ticks += GetInstructionReadTicks(pc); + } + // instruction is decoded now block->instructions.push_back(cbi); pc += sizeof(cbi.instruction.bits); diff --git a/src/core/cpu_code_cache.h b/src/core/cpu_code_cache.h index eec01ac3b..068e6706e 100644 --- a/src/core/cpu_code_cache.h +++ b/src/core/cpu_code_cache.h @@ -61,6 +61,8 @@ struct CodeBlock std::vector link_predecessors; std::vector link_successors; + TickCount uncached_fetch_ticks = 0; + u32 icache_line_count = 0; bool invalidated = false; const u32 GetPC() const { return key.GetPC(); } diff --git a/src/core/cpu_core.cpp b/src/core/cpu_core.cpp index ea90a1fb5..3dd02cd70 100644 --- a/src/core/cpu_core.cpp +++ b/src/core/cpu_core.cpp @@ -80,6 +80,8 @@ void Reset() g_state.cop0_regs.sr.bits = 0; g_state.cop0_regs.cause.bits = 0; + ClearICache(); + GTE::Reset(); SetPC(RESET_VECTOR); @@ -117,14 +119,17 @@ bool DoState(StateWrapper& sw) sw.Do(&g_state.load_delay_value); sw.Do(&g_state.next_load_delay_reg); sw.Do(&g_state.next_load_delay_value); - sw.Do(&g_state.cache_control); + sw.Do(&g_state.cache_control.bits); sw.DoBytes(g_state.dcache.data(), g_state.dcache.size()); if (!GTE::DoState(sw)) return false; if (sw.IsReading()) + { + ClearICache(); PGXP::Initialize(); + } return !sw.HasError(); } @@ -1416,7 +1421,6 @@ void InterpretCachedBlock(const CodeBlock& block) { // set up the state so we've already fetched the instruction DebugAssert(g_state.regs.pc == block.GetPC()); - g_state.regs.npc = block.GetPC() + 4; for (const CodeBlockInstruction& cbi : block.instructions) diff --git a/src/core/cpu_core.h b/src/core/cpu_core.h index 660596353..43c14c99a 100644 --- a/src/core/cpu_core.h +++ b/src/core/cpu_core.h @@ -19,7 +19,32 @@ enum : PhysicalMemoryAddress DCACHE_LOCATION = UINT32_C(0x1F800000), DCACHE_LOCATION_MASK = UINT32_C(0xFFFFFC00), DCACHE_OFFSET_MASK = UINT32_C(0x000003FF), - DCACHE_SIZE = UINT32_C(0x00000400) + DCACHE_SIZE = UINT32_C(0x00000400), + ICACHE_SIZE = UINT32_C(0x00001000), + ICACHE_SLOTS = ICACHE_SIZE / sizeof(u32), + ICACHE_LINE_SIZE = 16, + ICACHE_LINES = ICACHE_SIZE / ICACHE_LINE_SIZE, + ICACHE_SLOTS_PER_LINE = ICACHE_SLOTS / ICACHE_LINES, + ICACHE_TAG_ADDRESS_MASK = 0xFFFFFFF0u +}; + +enum : u32 +{ + ICACHE_DISABLED_BIT = 0x01, + ICACHE_INVALD_BIT = 0x02, +}; + +union CacheControl +{ + u32 bits; + + BitField lock_mode; + BitField invalidate_mode; + BitField tag_test_mode; + BitField dcache_scratchpad; + BitField dcache_enable; + BitField icache_fill_size; // actually dcache? icache always fills to 16 bytes + BitField icache_enable; }; struct State @@ -49,13 +74,15 @@ struct State Reg next_load_delay_reg = Reg::count; u32 next_load_delay_value = 0; - u32 cache_control = 0; + CacheControl cache_control{ 0 }; // GTE registers are stored here so we can access them on ARM with a single instruction GTE::Regs gte_regs = {}; // data cache (used as scratchpad) std::array dcache = {}; + std::array icache_tags = {}; + std::array icache_data = {}; }; extern State g_state; @@ -64,6 +91,7 @@ void Initialize(); void Shutdown(); void Reset(); bool DoState(StateWrapper& sw); +void ClearICache(); /// Executes interpreter loop. void Execute(); diff --git a/src/core/cpu_core_private.h b/src/core/cpu_core_private.h index 41ad24ec5..9f74fd7f0 100644 --- a/src/core/cpu_core_private.h +++ b/src/core/cpu_core_private.h @@ -34,8 +34,38 @@ ALWAYS_INLINE static void DispatchInterrupt() g_state.regs.pc); } +// icache stuff +ALWAYS_INLINE bool IsCachedAddress(VirtualMemoryAddress address) +{ + // KUSEG, KSEG0 + return (address >> 29) <= 4; +} +ALWAYS_INLINE u32 GetICacheLine(VirtualMemoryAddress address) +{ + return ((address >> 4) & 0xFFu); +} +ALWAYS_INLINE u32 GetICacheLineOffset(VirtualMemoryAddress address) +{ + return (address & (ICACHE_LINE_SIZE - 1)); +} +ALWAYS_INLINE u32 GetICacheTagForAddress(VirtualMemoryAddress address) +{ + return (address & ICACHE_TAG_ADDRESS_MASK); +} +ALWAYS_INLINE bool CompareICacheTag(VirtualMemoryAddress address) +{ + const u32 line = GetICacheLine(address); + return (g_state.icache_tags[line] == GetICacheTagForAddress(address)); +} + +TickCount GetInstructionReadTicks(VirtualMemoryAddress address); +TickCount GetICacheFillTicks(VirtualMemoryAddress address); +u32 FillICache(VirtualMemoryAddress address); +void CheckAndUpdateICacheTags(u32 line_count, TickCount uncached_ticks); + // defined in cpu_memory.cpp - memory access functions which return false if an exception was thrown. bool FetchInstruction(); +bool SafeReadInstruction(VirtualMemoryAddress addr, u32* value); bool ReadMemoryByte(VirtualMemoryAddress addr, u8* value); bool ReadMemoryHalfWord(VirtualMemoryAddress addr, u16* value); bool ReadMemoryWord(VirtualMemoryAddress addr, u32* value); diff --git a/src/core/cpu_recompiler_code_generator.cpp b/src/core/cpu_recompiler_code_generator.cpp index 1c7ae0db4..f350844a3 100644 --- a/src/core/cpu_recompiler_code_generator.cpp +++ b/src/core/cpu_recompiler_code_generator.cpp @@ -34,7 +34,7 @@ bool CodeGenerator::CompileBlock(const CodeBlock* block, CodeBlock::HostCodePoin const CodeBlockInstruction* cbi = m_block_start; while (cbi != m_block_end) { -#ifndef Y_BUILD_CONFIG_RELEASE +#ifdef _DEBUG SmallString disasm; DisassembleInstruction(&disasm, cbi->pc, cbi->instruction.bits, nullptr); Log_DebugPrintf("Compiling instruction '%s'", disasm.GetCharArray()); @@ -840,6 +840,9 @@ void CodeGenerator::BlockPrologue() { EmitStoreCPUStructField(offsetof(State, exception_raised), Value::FromConstantU8(0)); + if (m_block->uncached_fetch_ticks > 0) + EmitICacheCheckAndUpdate(); + // we don't know the state of the last block, so assume load delays might be in progress // TODO: Pull load delay into register cache m_current_instruction_in_branch_delay_slot_dirty = true; diff --git a/src/core/cpu_recompiler_code_generator.h b/src/core/cpu_recompiler_code_generator.h index b59992330..438786bd3 100644 --- a/src/core/cpu_recompiler_code_generator.h +++ b/src/core/cpu_recompiler_code_generator.h @@ -61,6 +61,7 @@ public: void EmitFlushInterpreterLoadDelay(); void EmitMoveNextInterpreterLoadDelay(); void EmitCancelInterpreterLoadDelayForReg(Reg reg); + void EmitICacheCheckAndUpdate(); void EmitLoadCPUStructField(HostReg host_reg, RegSize size, u32 offset); void EmitStoreCPUStructField(u32 offset, const Value& value); void EmitAddCPUStructField(u32 offset, const Value& value); diff --git a/src/core/cpu_recompiler_code_generator_generic.cpp b/src/core/cpu_recompiler_code_generator_generic.cpp index b652cb24b..c9e9a7ee6 100644 --- a/src/core/cpu_recompiler_code_generator_generic.cpp +++ b/src/core/cpu_recompiler_code_generator_generic.cpp @@ -22,4 +22,48 @@ void CodeGenerator::EmitStoreInterpreterLoadDelay(Reg reg, const Value& value) m_load_delay_dirty = true; } +#ifndef CPU_X64 + +void CodeGenerator::EmitICacheCheckAndUpdate() +{ + Value pc = CalculatePC(); + Value temp = m_register_cache.AllocateScratch(RegSize_32); + m_register_cache.InhibitAllocation(); + + EmitShr(temp.GetHostRegister(), pc.GetHostRegister(), RegSize_32, Value::FromConstantU32(29)); + LabelType is_cached; + LabelType ready_to_execute; + EmitConditionalBranch(Condition::LessEqual, false, temp.GetHostRegister(), Value::FromConstantU32(4), &is_cached); + EmitAddCPUStructField(offsetof(State, pending_ticks), + Value::FromConstantU32(static_cast(m_block->uncached_fetch_ticks))); + EmitBranch(&ready_to_execute); + EmitBindLabel(&is_cached); + + // cached path + EmitAnd(pc.GetHostRegister(), pc.GetHostRegister(), Value::FromConstantU32(ICACHE_TAG_ADDRESS_MASK)); + VirtualMemoryAddress current_address = (m_block->instructions[0].pc & ICACHE_TAG_ADDRESS_MASK); + for (u32 i = 0; i < m_block->icache_line_count; i++, current_address += ICACHE_LINE_SIZE) + { + const TickCount fill_ticks = GetICacheFillTicks(current_address); + if (fill_ticks <= 0) + continue; + + const u32 line = GetICacheLine(current_address); + const u32 offset = offsetof(State, icache_tags) + (line * sizeof(u32)); + LabelType cache_hit; + + EmitLoadCPUStructField(temp.GetHostRegister(), RegSize_32, offset); + EmitConditionalBranch(Condition::Equal, false, temp.GetHostRegister(), pc, &cache_hit); + EmitAddCPUStructField(offsetof(State, pending_ticks), Value::FromConstantU32(static_cast(fill_ticks))); + EmitStoreCPUStructField(offset, pc); + EmitBindLabel(&cache_hit); + EmitAdd(pc.GetHostRegister(), pc.GetHostRegister(), Value::FromConstantU32(ICACHE_LINE_SIZE), false); + } + + EmitBindLabel(&ready_to_execute); + m_register_cache.UnunhibitAllocation(); +} + +#endif + } // namespace CPU::Recompiler diff --git a/src/core/cpu_recompiler_code_generator_x64.cpp b/src/core/cpu_recompiler_code_generator_x64.cpp index e6f85e3e6..fd2f34035 100644 --- a/src/core/cpu_recompiler_code_generator_x64.cpp +++ b/src/core/cpu_recompiler_code_generator_x64.cpp @@ -2187,6 +2187,52 @@ void CodeGenerator::EmitCancelInterpreterLoadDelayForReg(Reg reg) m_emit->L(skip_cancel); } +void CodeGenerator::EmitICacheCheckAndUpdate() +{ + Value pc = CalculatePC(); + Value seg = m_register_cache.AllocateScratch(RegSize_32); + m_register_cache.InhibitAllocation(); + + m_emit->mov(GetHostReg32(seg), GetHostReg32(pc)); + m_emit->shr(GetHostReg32(seg), 29); + + Xbyak::Label is_cached; + m_emit->cmp(GetHostReg32(seg), 4); + m_emit->jle(is_cached); + + // uncached + Xbyak::Label done; + m_emit->add(m_emit->dword[GetCPUPtrReg() + offsetof(State, pending_ticks)], + static_cast(m_block->uncached_fetch_ticks)); + m_emit->jmp(done, Xbyak::CodeGenerator::T_NEAR); + + // cached + m_emit->L(is_cached); + m_emit->and_(GetHostReg32(pc), ICACHE_TAG_ADDRESS_MASK); + + VirtualMemoryAddress current_address = (m_block->instructions[0].pc & ICACHE_TAG_ADDRESS_MASK); + for (u32 i = 0; i < m_block->icache_line_count; i++, current_address += ICACHE_LINE_SIZE) + { + const TickCount fill_ticks = GetICacheFillTicks(current_address); + if (fill_ticks <= 0) + continue; + + const u32 line = GetICacheLine(current_address); + const u32 offset = offsetof(State, icache_tags) + (line * sizeof(u32)); + Xbyak::Label cache_hit; + + m_emit->cmp(GetHostReg32(pc), m_emit->dword[GetCPUPtrReg() + offset]); + m_emit->je(cache_hit); + m_emit->mov(m_emit->dword[GetCPUPtrReg() + offset], GetHostReg32(pc)); + m_emit->add(m_emit->dword[GetCPUPtrReg() + offsetof(State, pending_ticks)], static_cast(fill_ticks)); + m_emit->L(cache_hit); + m_emit->add(GetHostReg32(pc), ICACHE_LINE_SIZE); + } + + m_emit->L(done); + m_register_cache.UnunhibitAllocation(); +} + void CodeGenerator::EmitBranch(const void* address, bool allow_scratch) { const s64 jump_distance = diff --git a/src/core/cpu_recompiler_thunks.h b/src/core/cpu_recompiler_thunks.h index 602f522af..f698a859d 100644 --- a/src/core/cpu_recompiler_thunks.h +++ b/src/core/cpu_recompiler_thunks.h @@ -14,6 +14,7 @@ namespace Recompiler::Thunks { ////////////////////////////////////////////////////////////////////////// bool InterpretInstruction(); bool InterpretInstructionPGXP(); +void CheckAndUpdateICache(u32 pc, u32 line_count); // Memory access functions for the JIT - MSB is set on exception. u64 ReadMemoryByte(u32 address); diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index db8efc625..9322a5f62 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -362,6 +362,7 @@ void HostInterface::SetDefaultSettings(SettingsInterface& si) si.SetStringValue("CPU", "ExecutionMode", Settings::GetCPUExecutionModeName(Settings::DEFAULT_CPU_EXECUTION_MODE)); si.SetBoolValue("CPU", "RecompilerMemoryExceptions", false); + si.SetBoolValue("CPU", "ICache", false); si.SetStringValue("GPU", "Renderer", Settings::GetRendererName(Settings::DEFAULT_GPU_RENDERER)); si.SetIntValue("GPU", "ResolutionScale", 1); @@ -452,7 +453,8 @@ void HostInterface::FixIncompatibleSettings(bool display_osd_messages) { if (display_osd_messages) { - AddOSDMessage(TranslateStdString("OSDMessage", "PGXP is incompatible with the software renderer, disabling PGXP."), 10.0f); + AddOSDMessage( + TranslateStdString("OSDMessage", "PGXP is incompatible with the software renderer, disabling PGXP."), 10.0f); } g_settings.gpu_pgxp_enable = false; } @@ -510,6 +512,8 @@ void HostInterface::CheckForSettingsChanges(const Settings& old_settings) AddFormattedOSDMessage(5.0f, "Switching to %s CPU execution mode.", Settings::GetCPUExecutionModeName(g_settings.cpu_execution_mode)); CPU::CodeCache::SetUseRecompiler(g_settings.cpu_execution_mode == CPUExecutionMode::Recompiler); + CPU::CodeCache::Flush(); + CPU::ClearICache(); } if (g_settings.cpu_execution_mode == CPUExecutionMode::Recompiler && @@ -520,6 +524,15 @@ void HostInterface::CheckForSettingsChanges(const Settings& old_settings) CPU::CodeCache::Flush(); } + if (g_settings.cpu_execution_mode != CPUExecutionMode::Interpreter && + g_settings.cpu_recompiler_icache != old_settings.cpu_recompiler_icache) + { + AddFormattedOSDMessage(5.0f, "CPU ICache %s, flushing all blocks.", + g_settings.cpu_recompiler_icache ? "enabled" : "disabled"); + CPU::CodeCache::Flush(); + CPU::ClearICache(); + } + m_audio_stream->SetOutputVolume(g_settings.audio_output_muted ? 0 : g_settings.audio_output_volume); if (g_settings.gpu_resolution_scale != old_settings.gpu_resolution_scale || diff --git a/src/core/settings.cpp b/src/core/settings.cpp index e0254e3c8..21cdbc4b1 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -92,6 +92,7 @@ void Settings::Load(SettingsInterface& si) si.GetStringValue("CPU", "ExecutionMode", GetCPUExecutionModeName(DEFAULT_CPU_EXECUTION_MODE)).c_str()) .value_or(DEFAULT_CPU_EXECUTION_MODE); cpu_recompiler_memory_exceptions = si.GetBoolValue("CPU", "RecompilerMemoryExceptions", false); + cpu_recompiler_icache = si.GetBoolValue("CPU", "RecompilerICache", false); gpu_renderer = ParseRendererName(si.GetStringValue("GPU", "Renderer", GetRendererName(DEFAULT_GPU_RENDERER)).c_str()) .value_or(DEFAULT_GPU_RENDERER); @@ -206,6 +207,7 @@ void Settings::Save(SettingsInterface& si) const si.SetStringValue("CPU", "ExecutionMode", GetCPUExecutionModeName(cpu_execution_mode)); si.SetBoolValue("CPU", "RecompilerMemoryExceptions", cpu_recompiler_memory_exceptions); + si.SetBoolValue("CPU", "RecompilerICache", cpu_recompiler_icache); si.SetStringValue("GPU", "Renderer", GetRendererName(gpu_renderer)); si.SetStringValue("GPU", "Adapter", gpu_adapter.c_str()); diff --git a/src/core/settings.h b/src/core/settings.h index 91e26d5b4..5b98bde8e 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -69,6 +69,7 @@ struct Settings CPUExecutionMode cpu_execution_mode = CPUExecutionMode::Interpreter; bool cpu_recompiler_memory_exceptions = false; + bool cpu_recompiler_icache = false; float emulation_speed = 1.0f; bool speed_limiter_enabled = true; diff --git a/src/duckstation-libretro/libretro_host_interface.cpp b/src/duckstation-libretro/libretro_host_interface.cpp index a8fc3660b..8388ae868 100644 --- a/src/duckstation-libretro/libretro_host_interface.cpp +++ b/src/duckstation-libretro/libretro_host_interface.cpp @@ -370,7 +370,7 @@ void LibretroHostInterface::OnSystemDestroyed() m_using_hardware_renderer = false; } -static std::array s_option_definitions = {{ +static std::array s_option_definitions = {{ {"duckstation_Console.Region", "Console Region", "Determines which region/hardware to emulate. Auto-Detect will use the region of the disc inserted.", @@ -406,6 +406,12 @@ static std::array s_option_definitions = {{ "Which mode to use for CPU emulation. Recompiler provides the best performance.", {{"Interpreter", "Interpreter"}, {"CachedIntepreter", "Cached Interpreter"}, {"Recompiler", "Recompiler"}}, "Recompiler"}, + {"duckstation_CPU.RecompilerICache", + "CPU Recompiler ICache", + "Determines whether the CPU's instruction cache is simulated in the recompiler. Improves accuracy at a small cost " + "to performance. If games are running too fast, try enabling this option.", + {{"true", "Enabled"}, {"false", "Disabled"}}, + "false"}, {"duckstation_GPU.Renderer", "GPU Renderer", "Which renderer to use to emulate the GPU", diff --git a/src/duckstation-qt/advancedsettingswidget.cpp b/src/duckstation-qt/advancedsettingswidget.cpp index 2b4d93a32..837485555 100644 --- a/src/duckstation-qt/advancedsettingswidget.cpp +++ b/src/duckstation-qt/advancedsettingswidget.cpp @@ -27,6 +27,8 @@ AdvancedSettingsWidget::AdvancedSettingsWidget(QtHostInterface* host_interface, SettingWidgetBinder::BindWidgetToIntSetting(m_host_interface, m_ui.gpuMaxRunAhead, "Hacks", "GPUMaxRunAhead"); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.cpuRecompilerMemoryExceptions, "CPU", "RecompilerMemoryExceptions", false); + SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.cpuRecompilerICache, "CPU", "RecompilerICache", + false); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.showDebugMenu, "Main", "ShowDebugMenu"); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.gpuUseDebugDevice, "GPU", "UseDebugDevice"); @@ -38,6 +40,10 @@ AdvancedSettingsWidget::AdvancedSettingsWidget(QtHostInterface* host_interface, dialog->registerWidgetHelp(m_ui.gpuUseDebugDevice, tr("Use Debug Host GPU Device"), tr("Unchecked"), tr("Enables the usage of debug devices and shaders for rendering APIs which support them. " "Should only be used when debugging the emulator.")); + dialog->registerWidgetHelp( + m_ui.cpuRecompilerICache, tr("Enable Recompiler ICache"), tr("Unchecked"), + tr("Determines whether the CPU's instruction cache is simulated in the recompiler. Improves accuracy at a small " + "cost to performance. If games are running too fast, try enabling this option.")); } AdvancedSettingsWidget::~AdvancedSettingsWidget() = default; diff --git a/src/duckstation-qt/advancedsettingswidget.ui b/src/duckstation-qt/advancedsettingswidget.ui index d1ba75000..d75529513 100644 --- a/src/duckstation-qt/advancedsettingswidget.ui +++ b/src/duckstation-qt/advancedsettingswidget.ui @@ -184,6 +184,20 @@ + + + + Enable Recompiler Memory Exceptions + + + + + + + Enable Recompiler ICache + + + @@ -191,13 +205,6 @@ - - - - Enable Recompiler Memory Exceptions - - - diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp index 84b20e750..23d49de29 100644 --- a/src/duckstation-sdl/sdl_host_interface.cpp +++ b/src/duckstation-sdl/sdl_host_interface.cpp @@ -950,6 +950,11 @@ void SDLHostInterface::DrawDebugMenu() settings_changed |= ImGui::MenuItem("Show Timers State", nullptr, &debug_settings.show_timers_state); settings_changed |= ImGui::MenuItem("Show MDEC State", nullptr, &debug_settings.show_mdec_state); + ImGui::Separator(); + + settings_changed |= ImGui::MenuItem("Recompiler Memory Exceptions", nullptr, &m_settings_copy.cpu_recompiler_memory_exceptions); + settings_changed |= ImGui::MenuItem("Recompiler ICache", nullptr, &m_settings_copy.cpu_recompiler_icache); + if (settings_changed) { // have to apply it to the copy too, otherwise it won't save From 914f3ad447cf614385bbab4add177ef5c8d47e10 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 29 Aug 2020 22:11:10 +1000 Subject: [PATCH 23/61] GameSettings: Add trait for recompiler icache --- src/core/game_settings.cpp | 13 +++++++++++++ src/core/game_settings.h | 1 + 2 files changed, 14 insertions(+) diff --git a/src/core/game_settings.cpp b/src/core/game_settings.cpp index 1d84d5943..4e347f9e4 100644 --- a/src/core/game_settings.cpp +++ b/src/core/game_settings.cpp @@ -32,6 +32,7 @@ std::array, static_cast(Trait::Count)> {"ForcePGXPCPUMode", TRANSLATABLE("GameSettingsTrait", "Force PGXP CPU Mode")}, {"ForceDigitalController", TRANSLATABLE("GameSettingsTrait", "Force Digital Controller")}, {"ForceRecompilerMemoryExceptions", TRANSLATABLE("GameSettingsTrait", "Force Recompiler Memory Exceptions")}, + {"ForceRecompilerICache", TRANSLATABLE("GameSettingsTrait", "Force Recompiler ICache")}, }}; const char* GetTraitName(Trait trait) @@ -459,6 +460,18 @@ void Entry::ApplySettings(bool display_osd_messages) const g_settings.cpu_recompiler_memory_exceptions = true; } + + if (HasTrait(Trait::ForceRecompilerICache)) + { + if (display_osd_messages && g_settings.cpu_execution_mode != CPUExecutionMode::Interpreter && + !g_settings.cpu_recompiler_icache) + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "Recompiler ICache forced by game settings."), osd_duration); + } + + g_settings.cpu_recompiler_icache = true; + } } } // namespace GameSettings \ No newline at end of file diff --git a/src/core/game_settings.h b/src/core/game_settings.h index 45b8bd0ac..030e957e1 100644 --- a/src/core/game_settings.h +++ b/src/core/game_settings.h @@ -23,6 +23,7 @@ enum class Trait : u32 ForcePGXPCPUMode, ForceDigitalController, ForceRecompilerMemoryExceptions, + ForceRecompilerICache, Count }; From 547cc4dbf69bf53f49f50ef1f44c46a74821b51a Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 29 Aug 2020 22:19:09 +1000 Subject: [PATCH 24/61] Qt: Clear OSD messages on system stop --- src/duckstation-qt/qthostinterface.cpp | 1 + src/frontend-common/common_host_interface.cpp | 6 ++++++ src/frontend-common/common_host_interface.h | 1 + 3 files changed, 8 insertions(+) diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp index df1f4b6aa..a1b60a00b 100644 --- a/src/duckstation-qt/qthostinterface.cpp +++ b/src/duckstation-qt/qthostinterface.cpp @@ -641,6 +641,7 @@ void QtHostInterface::OnSystemDestroyed() { CommonHostInterface::OnSystemDestroyed(); + ClearOSDMessages(); startBackgroundControllerPollTimer(); emit emulationStopped(); } diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 73b7a87d3..26bbc4315 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -830,6 +830,12 @@ void CommonHostInterface::AddOSDMessage(std::string message, float duration /*= m_osd_messages.push_back(std::move(msg)); } +void CommonHostInterface::ClearOSDMessages() +{ + std::unique_lock lock(m_osd_messages_lock); + m_osd_messages.clear(); +} + void CommonHostInterface::DrawOSDMessages() { constexpr ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index 78007c3f3..cc2218d65 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -128,6 +128,7 @@ public: /// Adds OSD messages, duration is in seconds. void AddOSDMessage(std::string message, float duration = 2.0f) override; + void ClearOSDMessages(); /// Displays a loading screen with the logo, rendered with ImGui. Use when executing possibly-time-consuming tasks /// such as compiling shaders when starting up. From 057bf986c4f6adca945b1bfb2e7d9bf128d8300e Mon Sep 17 00:00:00 2001 From: Silent Date: Sat, 29 Aug 2020 14:19:28 +0200 Subject: [PATCH 25/61] Allow mapping half axes to buttons This allows to bind pressure sensitive NeGcon buttons to keyboard, mouse and controller buttons --- src/core/analog_controller.cpp | 8 +- src/core/controller.h | 8 +- src/core/negcon.cpp | 6 +- .../controllersettingswidget.cpp | 6 +- src/duckstation-qt/inputbindingdialog.cpp | 127 ++++++++++-------- src/duckstation-qt/inputbindingdialog.h | 24 ++-- src/duckstation-qt/inputbindingwidgets.cpp | 127 ++++++++++-------- src/duckstation-qt/inputbindingwidgets.h | 20 ++- src/duckstation-qt/main.cpp | 4 + src/duckstation-qt/qtutils.h | 3 + src/frontend-common/common_host_interface.cpp | 66 +++++++-- src/frontend-common/common_host_interface.h | 4 +- src/frontend-common/controller_interface.h | 3 +- .../sdl_controller_interface.cpp | 33 ++++- .../sdl_controller_interface.h | 9 +- .../xinput_controller_interface.cpp | 27 +++- .../xinput_controller_interface.h | 2 + 17 files changed, 306 insertions(+), 171 deletions(-) diff --git a/src/core/analog_controller.cpp b/src/core/analog_controller.cpp index 76038d77c..686fe0bad 100644 --- a/src/core/analog_controller.cpp +++ b/src/core/analog_controller.cpp @@ -466,10 +466,10 @@ std::optional AnalogController::StaticGetButtonCodeByName(std::string_view Controller::AxisList AnalogController::StaticGetAxisNames() { - return {{TRANSLATABLE("AnalogController", "LeftX"), static_cast(Axis::LeftX)}, - {TRANSLATABLE("AnalogController", "LeftY"), static_cast(Axis::LeftY)}, - {TRANSLATABLE("AnalogController", "RightX"), static_cast(Axis::RightX)}, - {TRANSLATABLE("AnalogController", "RightY"), static_cast(Axis::RightY)}}; + return {{TRANSLATABLE("AnalogController", "LeftX"), static_cast(Axis::LeftX), AxisType::Full}, + {TRANSLATABLE("AnalogController", "LeftY"), static_cast(Axis::LeftY), AxisType::Full}, + {TRANSLATABLE("AnalogController", "RightX"), static_cast(Axis::RightX), AxisType::Full}, + {TRANSLATABLE("AnalogController", "RightY"), static_cast(Axis::RightY), AxisType::Full}}; } Controller::ButtonList AnalogController::StaticGetButtonNames() diff --git a/src/core/controller.h b/src/core/controller.h index 3c63c3900..2f2a83eb4 100644 --- a/src/core/controller.h +++ b/src/core/controller.h @@ -14,8 +14,14 @@ class HostInterface; class Controller { public: + enum class AxisType : u8 + { + Full, + Half + }; + using ButtonList = std::vector>; - using AxisList = std::vector>; + using AxisList = std::vector>; using SettingList = std::vector; Controller(); diff --git a/src/core/negcon.cpp b/src/core/negcon.cpp index 85d235c41..d1c8e16c5 100644 --- a/src/core/negcon.cpp +++ b/src/core/negcon.cpp @@ -219,12 +219,12 @@ std::optional NeGcon::StaticGetButtonCodeByName(std::string_view button_nam Controller::AxisList NeGcon::StaticGetAxisNames() { -#define A(n) \ +#define A(n, t) \ { \ - #n, static_cast (Axis::n) \ + #n, static_cast (Axis::n), Controller::AxisType::t \ } - return {A(Steering), A(I), A(II), A(L)}; + return {A(Steering, Full), A(I, Half), A(II, Half), A(L, Half)}; #undef A } diff --git a/src/duckstation-qt/controllersettingswidget.cpp b/src/duckstation-qt/controllersettingswidget.cpp index 76783c03f..a66014163 100644 --- a/src/duckstation-qt/controllersettingswidget.cpp +++ b/src/duckstation-qt/controllersettingswidget.cpp @@ -224,7 +224,7 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin const int num_rows = (static_cast(axises.size()) + 1) / 2; int current_row = 0; int current_column = 0; - for (const auto& [axis_name, axis_code] : axises) + for (const auto& [axis_name, axis_code, axis_type] : axises) { if (current_row == num_rows) { @@ -235,8 +235,8 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin std::string section_name = StringUtil::StdStringFromFormat("Controller%d", index + 1); std::string key_name = StringUtil::StdStringFromFormat("Axis%s", axis_name.c_str()); QLabel* label = new QLabel(qApp->translate(cname, axis_name.c_str()), ui->bindings_container); - InputAxisBindingWidget* button = new InputAxisBindingWidget(m_host_interface, std::move(section_name), - std::move(key_name), ui->bindings_container); + InputAxisBindingWidget* button = new InputAxisBindingWidget( + m_host_interface, std::move(section_name), std::move(key_name), axis_type, ui->bindings_container); layout->addWidget(label, start_row + current_row, current_column); layout->addWidget(button, start_row + current_row, current_column + 1); diff --git a/src/duckstation-qt/inputbindingdialog.cpp b/src/duckstation-qt/inputbindingdialog.cpp index 56b261152..0edded305 100644 --- a/src/duckstation-qt/inputbindingdialog.cpp +++ b/src/duckstation-qt/inputbindingdialog.cpp @@ -36,8 +36,32 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event) { const QEvent::Type event_type = event->type(); - if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonRelease || - event_type == QEvent::MouseButtonDblClick) + // if the key is being released, set the input + if (event_type == QEvent::KeyRelease) + { + addNewBinding(std::move(m_new_binding_value)); + stopListeningForInput(); + return true; + } + else if (event_type == QEvent::KeyPress) + { + QString binding = QtUtils::KeyEventToString(static_cast(event)); + if (!binding.isEmpty()) + m_new_binding_value = QStringLiteral("Keyboard/%1").arg(binding).toStdString(); + + return true; + } + else if (event_type == QEvent::MouseButtonRelease) + { + const u32 button_mask = static_cast(static_cast(event)->button()); + const u32 button_index = (button_mask == 0u) ? 0 : CountTrailingZeros(button_mask); + m_new_binding_value = StringUtil::StdStringFromFormat("Mouse/Button%d", button_index + 1); + addNewBinding(std::move(m_new_binding_value)); + stopListeningForInput(); + return true; + } + + if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick) { return true; } @@ -103,6 +127,27 @@ void InputBindingDialog::addNewBinding(std::string new_binding) saveListToSettings(); } +void InputBindingDialog::bindToControllerAxis(int controller_index, int axis_index, std::optional positive) +{ + const char* sign_char = ""; + if (positive) + { + sign_char = *positive ? "+" : "-"; + } + + std::string binding = + StringUtil::StdStringFromFormat("Controller%d/%sAxis%d", controller_index, sign_char, axis_index); + addNewBinding(std::move(binding)); + stopListeningForInput(); +} + +void InputBindingDialog::bindToControllerButton(int controller_index, int button_index) +{ + std::string binding = StringUtil::StdStringFromFormat("Controller%d/Button%d", controller_index, button_index); + addNewBinding(std::move(binding)); + stopListeningForInput(); +} + void InputBindingDialog::onAddBindingButtonClicked() { if (isListeningForInput()) @@ -159,38 +204,6 @@ InputButtonBindingDialog::~InputButtonBindingDialog() InputButtonBindingDialog::stopListeningForInput(); } -bool InputButtonBindingDialog::eventFilter(QObject* watched, QEvent* event) -{ - const QEvent::Type event_type = event->type(); - - // if the key is being released, set the input - if (event_type == QEvent::KeyRelease) - { - addNewBinding(std::move(m_new_binding_value)); - stopListeningForInput(); - return true; - } - else if (event_type == QEvent::KeyPress) - { - QString binding = QtUtils::KeyEventToString(static_cast(event)); - if (!binding.isEmpty()) - m_new_binding_value = QStringLiteral("Keyboard/%1").arg(binding).toStdString(); - - return true; - } - else if (event_type == QEvent::MouseButtonRelease) - { - const u32 button_mask = static_cast(static_cast(event)->button()); - const u32 button_index = (button_mask == 0u) ? 0 : CountTrailingZeros(button_mask); - m_new_binding_value = StringUtil::StdStringFromFormat("Mouse/Button%d", button_index + 1); - addNewBinding(std::move(m_new_binding_value)); - stopListeningForInput(); - return true; - } - - return InputBindingDialog::eventFilter(watched, event); -} - void InputButtonBindingDialog::hookControllerInput() { ControllerInterface* controller_interface = m_host_interface->getControllerInterface(); @@ -206,7 +219,7 @@ void InputButtonBindingDialog::hookControllerInput() // TODO: this probably should consider the "last value" QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index), - Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, ei.value > 0)); + Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional, ei.value > 0)); return ControllerInterface::Hook::CallbackResult::StopMonitoring; } else if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f) @@ -229,21 +242,6 @@ void InputButtonBindingDialog::unhookControllerInput() controller_interface->ClearHook(); } -void InputButtonBindingDialog::bindToControllerAxis(int controller_index, int axis_index, bool positive) -{ - std::string binding = - StringUtil::StdStringFromFormat("Controller%d/%cAxis%d", controller_index, positive ? '+' : '-', axis_index); - addNewBinding(std::move(binding)); - stopListeningForInput(); -} - -void InputButtonBindingDialog::bindToControllerButton(int controller_index, int button_index) -{ - std::string binding = StringUtil::StdStringFromFormat("Controller%d/Button%d", controller_index, button_index); - addNewBinding(std::move(binding)); - stopListeningForInput(); -} - void InputButtonBindingDialog::startListeningForInput(u32 timeout_in_seconds) { InputBindingDialog::startListeningForInput(timeout_in_seconds); @@ -257,8 +255,10 @@ void InputButtonBindingDialog::stopListeningForInput() } InputAxisBindingDialog::InputAxisBindingDialog(QtHostInterface* host_interface, std::string section_name, - std::string key_name, std::vector bindings, QWidget* parent) - : InputBindingDialog(host_interface, std::move(section_name), std::move(key_name), std::move(bindings), parent) + std::string key_name, std::vector bindings, + Controller::AxisType axis_type, QWidget* parent) + : InputBindingDialog(host_interface, std::move(section_name), std::move(key_name), std::move(bindings), parent), + m_axis_type(axis_type) { } @@ -282,6 +282,13 @@ void InputAxisBindingDialog::hookControllerInput() return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index), + Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional, std::nullopt)); + return ControllerInterface::Hook::CallbackResult::StopMonitoring; + } + else if (ei.type == ControllerInterface::Hook::Type::Button && m_axis_type == Controller::AxisType::Half && + ei.value > 0.0f) + { + QMetaObject::invokeMethod(this, "bindToControllerButton", Q_ARG(int, ei.controller_index), Q_ARG(int, ei.button_or_axis_number)); return ControllerInterface::Hook::CallbackResult::StopMonitoring; } @@ -299,11 +306,19 @@ void InputAxisBindingDialog::unhookControllerInput() controller_interface->ClearHook(); } -void InputAxisBindingDialog::bindToControllerAxis(int controller_index, int axis_index) +bool InputAxisBindingDialog::eventFilter(QObject* watched, QEvent* event) { - std::string binding = StringUtil::StdStringFromFormat("Controller%d/Axis%d", controller_index, axis_index); - addNewBinding(std::move(binding)); - stopListeningForInput(); + if (m_axis_type != Controller::AxisType::Half) + { + const QEvent::Type event_type = event->type(); + + if (event_type == QEvent::KeyRelease || event_type == QEvent::KeyPress || event_type == QEvent::MouseButtonRelease) + { + return true; + } + } + + return InputBindingDialog::eventFilter(watched, event); } void InputAxisBindingDialog::startListeningForInput(u32 timeout_in_seconds) diff --git a/src/duckstation-qt/inputbindingdialog.h b/src/duckstation-qt/inputbindingdialog.h index 84ee7efe0..ff26b3776 100644 --- a/src/duckstation-qt/inputbindingdialog.h +++ b/src/duckstation-qt/inputbindingdialog.h @@ -1,7 +1,9 @@ #pragma once #include "common/types.h" +#include "core/controller.h" #include "ui_inputbindingdialog.h" #include +#include #include #include @@ -17,6 +19,8 @@ public: ~InputBindingDialog(); protected Q_SLOTS: + void bindToControllerAxis(int controller_index, int axis_index, std::optional positive); + void bindToControllerButton(int controller_index, int button_index); void onAddBindingButtonClicked(); void onRemoveBindingButtonClicked(); void onClearBindingsButtonClicked(); @@ -52,7 +56,7 @@ protected: u32 m_input_listen_remaining_seconds = 0; }; -class InputButtonBindingDialog : public InputBindingDialog +class InputButtonBindingDialog final : public InputBindingDialog { Q_OBJECT @@ -61,13 +65,6 @@ public: std::vector bindings, QWidget* parent); ~InputButtonBindingDialog(); -protected: - bool eventFilter(QObject* watched, QEvent* event) override; - -private Q_SLOTS: - void bindToControllerAxis(int controller_index, int axis_index, bool positive); - void bindToControllerButton(int controller_index, int button_index); - protected: void startListeningForInput(u32 timeout_in_seconds) override; void stopListeningForInput() override; @@ -75,21 +72,22 @@ protected: void unhookControllerInput(); }; -class InputAxisBindingDialog : public InputBindingDialog +class InputAxisBindingDialog final : public InputBindingDialog { Q_OBJECT public: InputAxisBindingDialog(QtHostInterface* host_interface, std::string section_name, std::string key_name, - std::vector bindings, QWidget* parent); + std::vector bindings, Controller::AxisType axis_type, QWidget* parent); ~InputAxisBindingDialog(); -private Q_SLOTS: - void bindToControllerAxis(int controller_index, int axis_index); - protected: + bool eventFilter(QObject* watched, QEvent* event) override; void startListeningForInput(u32 timeout_in_seconds) override; void stopListeningForInput() override; void hookControllerInput(); void unhookControllerInput(); + +private: + Controller::AxisType m_axis_type; }; diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp index ec97936dc..9d55dc16e 100644 --- a/src/duckstation-qt/inputbindingwidgets.cpp +++ b/src/duckstation-qt/inputbindingwidgets.cpp @@ -40,6 +40,27 @@ void InputBindingWidget::updateText() setText(QString::fromStdString(m_bindings[0])); } +void InputBindingWidget::bindToControllerAxis(int controller_index, int axis_index, std::optional positive) +{ + const char* sign_char = ""; + if (positive) + { + sign_char = *positive ? "+" : "-"; + } + + m_new_binding_value = + StringUtil::StdStringFromFormat("Controller%d/%sAxis%d", controller_index, sign_char, axis_index); + setNewBinding(); + stopListeningForInput(); +} + +void InputBindingWidget::bindToControllerButton(int controller_index, int button_index) +{ + m_new_binding_value = StringUtil::StdStringFromFormat("Controller%d/Button%d", controller_index, button_index); + setNewBinding(); + stopListeningForInput(); +} + void InputBindingWidget::beginRebindAll() { m_is_binding_all = true; @@ -53,8 +74,32 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event) { const QEvent::Type event_type = event->type(); - if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonRelease || - event_type == QEvent::MouseButtonDblClick) + // if the key is being released, set the input + if (event_type == QEvent::KeyRelease) + { + setNewBinding(); + stopListeningForInput(); + return true; + } + else if (event_type == QEvent::KeyPress) + { + QString binding = QtUtils::KeyEventToString(static_cast(event)); + if (!binding.isEmpty()) + m_new_binding_value = QStringLiteral("Keyboard/%1").arg(binding).toStdString(); + + return true; + } + else if (event_type == QEvent::MouseButtonRelease) + { + const u32 button_mask = static_cast(static_cast(event)->button()); + const u32 button_index = (button_mask == 0u) ? 0 : CountTrailingZeros(button_mask); + m_new_binding_value = StringUtil::StdStringFromFormat("Mouse/Button%d", button_index + 1); + setNewBinding(); + stopListeningForInput(); + return true; + } + + if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick) { return true; } @@ -185,38 +230,6 @@ InputButtonBindingWidget::~InputButtonBindingWidget() InputButtonBindingWidget::stopListeningForInput(); } -bool InputButtonBindingWidget::eventFilter(QObject* watched, QEvent* event) -{ - const QEvent::Type event_type = event->type(); - - // if the key is being released, set the input - if (event_type == QEvent::KeyRelease) - { - setNewBinding(); - stopListeningForInput(); - return true; - } - else if (event_type == QEvent::KeyPress) - { - QString binding = QtUtils::KeyEventToString(static_cast(event)); - if (!binding.isEmpty()) - m_new_binding_value = QStringLiteral("Keyboard/%1").arg(binding).toStdString(); - - return true; - } - else if (event_type == QEvent::MouseButtonRelease) - { - const u32 button_mask = static_cast(static_cast(event)->button()); - const u32 button_index = (button_mask == 0u) ? 0 : CountTrailingZeros(button_mask); - m_new_binding_value = StringUtil::StdStringFromFormat("Mouse/Button%d", button_index + 1); - setNewBinding(); - stopListeningForInput(); - return true; - } - - return InputBindingWidget::eventFilter(watched, event); -} - void InputButtonBindingWidget::hookControllerInput() { ControllerInterface* controller_interface = m_host_interface->getControllerInterface(); @@ -232,7 +245,7 @@ void InputButtonBindingWidget::hookControllerInput() // TODO: this probably should consider the "last value" QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index), - Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, ei.value > 0)); + Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional, ei.value > 0)); return ControllerInterface::Hook::CallbackResult::StopMonitoring; } else if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f) @@ -255,21 +268,6 @@ void InputButtonBindingWidget::unhookControllerInput() controller_interface->ClearHook(); } -void InputButtonBindingWidget::bindToControllerAxis(int controller_index, int axis_index, bool positive) -{ - m_new_binding_value = - StringUtil::StdStringFromFormat("Controller%d/%cAxis%d", controller_index, positive ? '+' : '-', axis_index); - setNewBinding(); - stopListeningForInput(); -} - -void InputButtonBindingWidget::bindToControllerButton(int controller_index, int button_index) -{ - m_new_binding_value = StringUtil::StdStringFromFormat("Controller%d/Button%d", controller_index, button_index); - setNewBinding(); - stopListeningForInput(); -} - void InputButtonBindingWidget::startListeningForInput(u32 timeout_in_seconds) { InputBindingWidget::startListeningForInput(timeout_in_seconds); @@ -291,8 +289,8 @@ void InputButtonBindingWidget::openDialog() } InputAxisBindingWidget::InputAxisBindingWidget(QtHostInterface* host_interface, std::string section_name, - std::string key_name, QWidget* parent) - : InputBindingWidget(host_interface, std::move(section_name), std::move(key_name), parent) + std::string key_name, Controller::AxisType axis_type, QWidget* parent) + : InputBindingWidget(host_interface, std::move(section_name), std::move(key_name), parent), m_axis_type(axis_type) { } @@ -316,6 +314,13 @@ void InputAxisBindingWidget::hookControllerInput() return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index), + Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional, std::nullopt)); + return ControllerInterface::Hook::CallbackResult::StopMonitoring; + } + else if (ei.type == ControllerInterface::Hook::Type::Button && m_axis_type == Controller::AxisType::Half && + ei.value > 0.0f) + { + QMetaObject::invokeMethod(this, "bindToControllerButton", Q_ARG(int, ei.controller_index), Q_ARG(int, ei.button_or_axis_number)); return ControllerInterface::Hook::CallbackResult::StopMonitoring; } @@ -333,11 +338,19 @@ void InputAxisBindingWidget::unhookControllerInput() controller_interface->ClearHook(); } -void InputAxisBindingWidget::bindToControllerAxis(int controller_index, int axis_index) +bool InputAxisBindingWidget::eventFilter(QObject* watched, QEvent* event) { - m_new_binding_value = StringUtil::StdStringFromFormat("Controller%d/Axis%d", controller_index, axis_index); - setNewBinding(); - stopListeningForInput(); + if (m_axis_type != Controller::AxisType::Half) + { + const QEvent::Type event_type = event->type(); + + if (event_type == QEvent::KeyRelease || event_type == QEvent::KeyPress || event_type == QEvent::MouseButtonRelease) + { + return true; + } + } + + return InputBindingWidget::eventFilter(watched, event); } void InputAxisBindingWidget::startListeningForInput(u32 timeout_in_seconds) @@ -354,7 +367,7 @@ void InputAxisBindingWidget::stopListeningForInput() void InputAxisBindingWidget::openDialog() { - InputAxisBindingDialog binding_dialog(m_host_interface, m_section_name, m_key_name, m_bindings, + InputAxisBindingDialog binding_dialog(m_host_interface, m_section_name, m_key_name, m_bindings, m_axis_type, QtUtils::GetRootWidget(this)); binding_dialog.exec(); reloadBinding(); diff --git a/src/duckstation-qt/inputbindingwidgets.h b/src/duckstation-qt/inputbindingwidgets.h index 9c3bc338d..e34f44d8d 100644 --- a/src/duckstation-qt/inputbindingwidgets.h +++ b/src/duckstation-qt/inputbindingwidgets.h @@ -1,6 +1,8 @@ #pragma once +#include "core/controller.h" #include "core/types.h" #include +#include class QTimer; @@ -18,6 +20,8 @@ public: ALWAYS_INLINE void setNextWidget(InputBindingWidget* widget) { m_next_widget = widget; } public Q_SLOTS: + void bindToControllerAxis(int controller_index, int axis_index, std::optional positive); + void bindToControllerButton(int controller_index, int button_index); void beginRebindAll(); void clearBinding(); void reloadBinding(); @@ -66,13 +70,6 @@ public: QWidget* parent); ~InputButtonBindingWidget(); -protected: - bool eventFilter(QObject* watched, QEvent* event) override; - -private Q_SLOTS: - void bindToControllerAxis(int controller_index, int axis_index, bool positive); - void bindToControllerButton(int controller_index, int button_index); - protected: void startListeningForInput(u32 timeout_in_seconds) override; void stopListeningForInput() override; @@ -87,18 +84,19 @@ class InputAxisBindingWidget : public InputBindingWidget public: InputAxisBindingWidget(QtHostInterface* host_interface, std::string section_name, std::string key_name, - QWidget* parent); + Controller::AxisType axis_type, QWidget* parent); ~InputAxisBindingWidget(); -private Q_SLOTS: - void bindToControllerAxis(int controller_index, int axis_index); - protected: + bool eventFilter(QObject* watched, QEvent* event) override; void startListeningForInput(u32 timeout_in_seconds) override; void stopListeningForInput() override; void openDialog() override; void hookControllerInput(); void unhookControllerInput(); + +private: + Controller::AxisType m_axis_type; }; class InputRumbleBindingWidget : public InputBindingWidget diff --git a/src/duckstation-qt/main.cpp b/src/duckstation-qt/main.cpp index 8eb740aaf..cfad70bea 100644 --- a/src/duckstation-qt/main.cpp +++ b/src/duckstation-qt/main.cpp @@ -1,6 +1,7 @@ #include "common/log.h" #include "mainwindow.h" #include "qthostinterface.h" +#include "qtutils.h" #include #include #include @@ -8,6 +9,9 @@ int main(int argc, char* argv[]) { + // Register any standard types we need elsewhere + qRegisterMetaType>(); + QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); diff --git a/src/duckstation-qt/qtutils.h b/src/duckstation-qt/qtutils.h index ef9975616..5eb42093a 100644 --- a/src/duckstation-qt/qtutils.h +++ b/src/duckstation-qt/qtutils.h @@ -1,9 +1,12 @@ #pragma once #include +#include #include #include #include +Q_DECLARE_METATYPE(std::optional); + class ByteStream; class QFrame; diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 73b7a87d3..30c913def 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -7,7 +7,6 @@ #include "common/string_util.h" #include "controller_interface.h" #include "core/cdrom.h" -#include "core/controller.h" #include "core/cpu_code_cache.h" #include "core/dma.h" #include "core/game_list.h" @@ -1055,8 +1054,9 @@ void CommonHostInterface::UpdateControllerInputMap(SettingsInterface& si) const auto axis_names = Controller::GetAxisNames(ctype); for (const auto& it : axis_names) { - const std::string& axis_name = it.first; - const s32 axis_code = it.second; + const std::string& axis_name = std::get(it); + const s32 axis_code = std::get(it); + const auto axis_type = std::get(it); const std::vector bindings = si.GetStringList(category, TinyString::FromFormat("Axis%s", axis_name.c_str())); @@ -1066,7 +1066,7 @@ void CommonHostInterface::UpdateControllerInputMap(SettingsInterface& si) if (!SplitBinding(binding, &device, &axis)) continue; - AddAxisToInputMap(binding, device, axis, [this, controller_index, axis_code](float value) { + AddAxisToInputMap(binding, device, axis, axis_type, [this, controller_index, axis_code](float value) { if (System::IsShutdown()) return; @@ -1198,8 +1198,44 @@ bool CommonHostInterface::AddButtonToInputMap(const std::string& binding, const } bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const std::string_view& device, - const std::string_view& axis, InputAxisHandler handler) + const std::string_view& axis, Controller::AxisType axis_type, + InputAxisHandler handler) { + if (axis_type == Controller::AxisType::Half) + { + if (device == "Keyboard") + { + std::optional key_id = GetHostKeyCode(axis); + if (!key_id.has_value()) + { + Log_WarningPrintf("Unknown keyboard key in binding '%s'", binding.c_str()); + return false; + } + + m_keyboard_input_handlers.emplace(key_id.value(), std::move(handler)); + return true; + } + + if (device == "Mouse") + { + if (StringUtil::StartsWith(axis, "Button")) + { + const std::optional button_index = StringUtil::FromChars(axis.substr(6)); + if (!button_index.has_value()) + { + Log_WarningPrintf("Invalid button in mouse binding '%s'", binding.c_str()); + return false; + } + + m_mouse_input_handlers.emplace(static_cast(button_index.value()), std::move(handler)); + return true; + } + + Log_WarningPrintf("Malformed mouse binding '%s'", binding.c_str()); + return false; + } + } + if (StringUtil::StartsWith(device, "Controller")) { if (!m_controller_interface) @@ -1227,6 +1263,18 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st return true; } + else if (StringUtil::StartsWith(axis, "Button") && axis_type == Controller::AxisType::Half) + { + const std::optional button_index = StringUtil::FromChars(axis.substr(6)); + if (!button_index || + !m_controller_interface->BindControllerButtonToAxis(*controller_index, *button_index, std::move(handler))) + { + Log_WarningPrintf("Failed to bind controller button '%s' to axis", binding.c_str()); + return false; + } + + return true; + } Log_WarningPrintf("Malformed controller binding '%s' in button", binding.c_str()); return false; @@ -1533,7 +1581,7 @@ void CommonHostInterface::ClearAllControllerBindings(SettingsInterface& si) si.DeleteValue(section_name, button.first.c_str()); for (const auto& axis : Controller::GetAxisNames(ctype)) - si.DeleteValue(section_name, axis.first.c_str()); + si.DeleteValue(section_name, std::get(axis).c_str()); if (Controller::GetVibrationMotorCount(ctype) > 0) si.DeleteValue(section_name, "Rumble"); @@ -1577,8 +1625,8 @@ void CommonHostInterface::ApplyInputProfile(const char* profile_path, SettingsIn for (const auto& axis : Controller::GetAxisNames(*ctype)) { - const auto key_name = TinyString::FromFormat("Axis%s", axis.first.c_str()); - si.DeleteValue(section_name, axis.first.c_str()); + const auto key_name = TinyString::FromFormat("Axis%s", std::get(axis).c_str()); + si.DeleteValue(section_name, std::get(axis).c_str()); const std::vector bindings = profile.GetStringList(section_name, key_name); for (const std::string& binding : bindings) si.AddToStringList(section_name, key_name, binding.c_str()); @@ -1636,7 +1684,7 @@ bool CommonHostInterface::SaveInputProfile(const char* profile_path, SettingsInt for (const auto& axis : Controller::GetAxisNames(ctype)) { - const auto key_name = TinyString::FromFormat("Axis%s", axis.first.c_str()); + const auto key_name = TinyString::FromFormat("Axis%s", std::get(axis).c_str()); const std::vector bindings = si.GetStringList(section_name, key_name); for (const std::string& binding : bindings) profile.AddToStringList(section_name, key_name, binding.c_str()); diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index 78007c3f3..7353365ad 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -1,5 +1,6 @@ #pragma once #include "common/string.h" +#include "core/controller.h" #include "core/host_interface.h" #include #include @@ -195,7 +196,8 @@ protected: virtual bool AddButtonToInputMap(const std::string& binding, const std::string_view& device, const std::string_view& button, InputButtonHandler handler); virtual bool AddAxisToInputMap(const std::string& binding, const std::string_view& device, - const std::string_view& axis, InputAxisHandler handler); + const std::string_view& axis, Controller::AxisType axis_type, + InputAxisHandler handler); virtual bool AddRumbleToInputMap(const std::string& binding, u32 controller_index, u32 num_motors); /// Reloads the input map from config. Callable from controller interface. diff --git a/src/frontend-common/controller_interface.h b/src/frontend-common/controller_interface.h index ab877a72a..60e287f11 100644 --- a/src/frontend-common/controller_interface.h +++ b/src/frontend-common/controller_interface.h @@ -3,9 +3,9 @@ #include "core/types.h" #include #include -#include #include #include +#include class HostInterface; class Controller; @@ -54,6 +54,7 @@ public: virtual bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) = 0; virtual bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, ButtonCallback callback) = 0; + virtual bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) = 0; virtual void PollEvents() = 0; diff --git a/src/frontend-common/sdl_controller_interface.cpp b/src/frontend-common/sdl_controller_interface.cpp index 919228d49..7d3818758 100644 --- a/src/frontend-common/sdl_controller_interface.cpp +++ b/src/frontend-common/sdl_controller_interface.cpp @@ -35,8 +35,7 @@ bool SDLControllerInterface::Initialize(CommonHostInterface* host_interface) Log_InfoPrintf("Loading game controller mappings from '%s'", gcdb_file_name.c_str()); if (SDL_GameControllerAddMappingsFromFile(gcdb_file_name.c_str()) < 0) { - Log_ErrorPrintf("SDL_GameControllerAddMappingsFromFile(%s) failed: %s", - gcdb_file_name.c_str(), SDL_GetError()); + Log_ErrorPrintf("SDL_GameControllerAddMappingsFromFile(%s) failed: %s", gcdb_file_name.c_str(), SDL_GetError()); } } @@ -293,6 +292,19 @@ bool SDLControllerInterface::BindControllerAxisToButton(int controller_index, in return true; } +bool SDLControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) +{ + auto it = GetControllerDataForPlayerId(controller_index); + if (it == m_controllers.end()) + return false; + + if (button_number < 0 || button_number >= MAX_NUM_BUTTONS) + return false; + + it->button_axis_mapping[button_number] = std::move(callback); + return true; +} + bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_Event* ev) { const float value = static_cast(ev->caxis.value) / (ev->caxis.value < 0 ? 32768.0f : 32767.0f); @@ -350,11 +362,20 @@ bool SDLControllerInterface::HandleControllerButtonEvent(const SDL_Event* ev) return true; const ButtonCallback& cb = it->button_mapping[ev->cbutton.button]; - if (!cb) - return false; + if (cb) + { + cb(pressed); + return true; + } - cb(pressed); - return true; + // Assume a half-axis, i.e. in 0..1 range + const AxisCallback& axis_cb = it->button_axis_mapping[ev->cbutton.button]; + if (axis_cb) + { + axis_cb(pressed ? 1.0f : 0.0f); + } + + return false; } u32 SDLControllerInterface::GetControllerRumbleMotorCount(int controller_index) diff --git a/src/frontend-common/sdl_controller_interface.h b/src/frontend-common/sdl_controller_interface.h index 4fa43e556..d839b73a0 100644 --- a/src/frontend-common/sdl_controller_interface.h +++ b/src/frontend-common/sdl_controller_interface.h @@ -1,10 +1,10 @@ #pragma once -#include "core/types.h" #include "controller_interface.h" +#include "core/types.h" #include #include -#include #include +#include union SDL_Event; @@ -27,7 +27,9 @@ public: // Binding to events. If a binding for this axis/button already exists, returns false. bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) override; bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override; - bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, ButtonCallback callback) override; + bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, + ButtonCallback callback) override; + bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override; // Changing rumble strength. u32 GetControllerRumbleMotorCount(int controller_index) override; @@ -59,6 +61,7 @@ private: std::array axis_mapping; std::array button_mapping; std::array, MAX_NUM_AXISES> axis_button_mapping; + std::array button_axis_mapping; }; using ControllerDataVector = std::vector; diff --git a/src/frontend-common/xinput_controller_interface.cpp b/src/frontend-common/xinput_controller_interface.cpp index 076746f3b..3ef93d90d 100644 --- a/src/frontend-common/xinput_controller_interface.cpp +++ b/src/frontend-common/xinput_controller_interface.cpp @@ -204,6 +204,19 @@ bool XInputControllerInterface::BindControllerAxisToButton(int controller_index, return true; } +bool XInputControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, + AxisCallback callback) +{ + if (static_cast(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected) + return false; + + if (button_number < 0 || button_number >= MAX_NUM_BUTTONS) + return false; + + m_controllers[controller_index].button_axis_mapping[button_number] = std::move(callback); + return true; +} + bool XInputControllerInterface::HandleAxisEvent(u32 index, Axis axis, s32 value) { const float f_value = static_cast(value) / (value < 0 ? 32768.0f : 32767.0f); @@ -255,10 +268,18 @@ bool XInputControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pr return true; const ButtonCallback& cb = m_controllers[index].button_mapping[button]; - if (!cb) - return false; + if (cb) + { + cb(pressed); + return true; + } - cb(pressed); + // Assume a half-axis, i.e. in 0..1 range + const AxisCallback& axis_cb = m_controllers[index].button_axis_mapping[button]; + if (axis_cb) + { + axis_cb(pressed ? 1.0f : 0.0f); + } return true; } diff --git a/src/frontend-common/xinput_controller_interface.h b/src/frontend-common/xinput_controller_interface.h index 5d02e1262..21802ff40 100644 --- a/src/frontend-common/xinput_controller_interface.h +++ b/src/frontend-common/xinput_controller_interface.h @@ -26,6 +26,7 @@ public: bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override; bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, ButtonCallback callback) override; + bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override; // Changing rumble strength. u32 GetControllerRumbleMotorCount(int controller_index) override; @@ -68,6 +69,7 @@ private: std::array axis_mapping; std::array button_mapping; std::array, MAX_NUM_AXISES> axis_button_mapping; + std::array button_axis_mapping; }; using ControllerDataArray = std::array; From d69c17db3d3528c319e471f78a936350fec086b7 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 29 Aug 2020 22:30:36 +1000 Subject: [PATCH 26/61] Force enable icache on a few games Fixes them or fixes their speed issues. --- data/database/compatibility.xml | 17 ++++++++++++----- data/database/gamesettings.ini | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/data/database/compatibility.xml b/data/database/compatibility.xml index 8a0b9d91a..7aeae3c98 100644 --- a/data/database/compatibility.xml +++ b/data/database/compatibility.xml @@ -732,7 +732,6 @@ No Issues 0.1-1600-g032127a7 - No Issues @@ -1189,9 +1188,9 @@ Tetris with Card Captor Sakura (Japan) Crashes In-Game - - Crashes In Intro - 0.1-1580-g136a9d60 + + No Issues + 0.1-1614-g914f3ad4 Blackscreen after the first Loading screen (Issue #54). @@ -1743,6 +1742,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1308-g622e50fa + + No Issues + 0.1-1614-g914f3ad4 + No Issues 0.1-1308-g622e50fa @@ -1917,6 +1920,10 @@ Tetris with Card Captor Sakura (Japan) No Issues + + No Issues + 0.1-1614-g914f3ad4 + No Issues Issue 419 @@ -3216,4 +3223,4 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1409-ge198e315 - \ No newline at end of file + diff --git a/data/database/gamesettings.ini b/data/database/gamesettings.ini index ccea6b7a7..cb53a06a9 100644 --- a/data/database/gamesettings.ini +++ b/data/database/gamesettings.ini @@ -138,3 +138,23 @@ ForceDigitalController = true # SLUS-00213 (Tekken 2 (USA)) [SLUS-00213] ForceDigitalController = true + + +# SLPS-00435 (Megatudo 2096 (Japan)) +[SLPS-00435] +ForceRecompilerICache = true + + +# SLUS-00388 (NBA Jam Extreme (USA)) +[SLUS-00388] +ForceRecompilerICache = true + + +# SCES-02834 (Crash Bash (Europe) (En,Fr,De,Es,It)) +[SCES-02834] +ForceRecompilerICache = true + + +# SLUS-00870 (Formula One 99 (USA) (En,Fr,Es)) +[SLUS-00870] +ForceInterpreter = true From 1a15cf49510cf648ca4bba139ac054122685d8e0 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 29 Aug 2020 22:33:44 +1000 Subject: [PATCH 27/61] Update compatibility list --- data/database/compatibility.xml | 66 ++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/data/database/compatibility.xml b/data/database/compatibility.xml index 7aeae3c98..0f3c059fb 100644 --- a/data/database/compatibility.xml +++ b/data/database/compatibility.xml @@ -1056,6 +1056,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-896-gc8a00c5 + + No Issues + 0.1-1608-g79aaf908 + No Issues 0.1-896-gc8a00c5 @@ -1090,6 +1094,10 @@ Tetris with Card Captor Sakura (Japan) The playable character gain "yellow horns" (actually, 2 broken polygons in head) if the upscaling is used (Issue 427). 0.1-1425-g05f0ce6d + + No Issues + 0.1-1608-g79aaf908 + No Issues If use fast forward some controllers buttons may not function properly. @@ -1099,6 +1107,18 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1336-gd711baa + + No Issues + 0.1-1608-g79aaf908 + + + No Issues + 0.1-1608-g79aaf908 + + + No Issues + 0.1-1608-g79aaf908 + No Issues 0.1-986-gfc911de1 @@ -1180,6 +1200,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-986-gfc911de1 + + No Issues + 0.1-1608-g79aaf908 + Graphical/Audio Issues 0.1-1580-g136a9d60 @@ -1216,6 +1240,10 @@ Tetris with Card Captor Sakura (Japan) No Issues + + No Issues + 0.1-1608-g79aaf908 + No Issues 0.1-1425-g05f0ce6d @@ -1247,6 +1275,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1308-g622e50fa + + No Issues + 0.1-1608-g79aaf908 + No Issues 0.1-1304-gc8b6712 @@ -1303,6 +1335,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-986-gfc911de1 + + No Issues + 0.1-1608-g79aaf908 + No Issues 0.1-1308-g622e50fa @@ -1315,6 +1351,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1425-g05f0ce6d + + No Issues + 0.1-1608-g79aaf908 + No Issues @@ -1983,6 +2023,14 @@ Tetris with Card Captor Sakura (Japan) 0.1-1443-g7ab521f7 After the initial FMV, the game crashes (Issue #717). + + No Issues + 0.1-1608-g79aaf908 + + + No Issues + 0.1-1608-g79aaf908 + No Issues 0.1-884-g096ed21 @@ -2393,6 +2441,10 @@ Tetris with Card Captor Sakura (Japan) 0.1-1334-g10f2366 The menu music is messing completely (Issue #662). + + No Issues + 0.1-1608-g79aaf908 + No Issues 0.1-1333-g5a955a4 @@ -3150,6 +3202,10 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1333-g5a955a4 + + No Issues + 0.1-1608-g79aaf908 + No Issues @@ -3158,6 +3214,10 @@ Tetris with Card Captor Sakura (Japan) Small grafics errors when the cursor is over the text (don't know if this bug occurs in the real hardware). 0.1-1304-gc8b6712 + + No Issues + 0.1-1608-g79aaf908 + No Issues 0.1-986-gfc911de1 @@ -3170,6 +3230,10 @@ Tetris with Card Captor Sakura (Japan) No Issues Sprite glitches + + No Issues + 0.1-1608-g79aaf908 + No Issues 0.1-1448-g472f1c1c @@ -3223,4 +3287,4 @@ Tetris with Card Captor Sakura (Japan) No Issues 0.1-1409-ge198e315 - + \ No newline at end of file From 800c5303de8cf6436e886533cd3829a5373e8c08 Mon Sep 17 00:00:00 2001 From: Silent Date: Sat, 29 Aug 2020 14:42:28 +0200 Subject: [PATCH 28/61] Give GamePropertiesDialog a parent --- src/duckstation-qt/gamepropertiesdialog.cpp | 4 ++-- src/duckstation-qt/gamepropertiesdialog.h | 2 +- src/duckstation-qt/mainwindow.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/duckstation-qt/gamepropertiesdialog.cpp b/src/duckstation-qt/gamepropertiesdialog.cpp index 2b76d6298..1cae9feb7 100644 --- a/src/duckstation-qt/gamepropertiesdialog.cpp +++ b/src/duckstation-qt/gamepropertiesdialog.cpp @@ -153,9 +153,9 @@ void GamePropertiesDialog::setupAdditionalUi() setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); } -void GamePropertiesDialog::showForEntry(QtHostInterface* host_interface, const GameListEntry* ge) +void GamePropertiesDialog::showForEntry(QtHostInterface* host_interface, const GameListEntry* ge, QWidget* parent) { - GamePropertiesDialog* gpd = new GamePropertiesDialog(host_interface); + GamePropertiesDialog* gpd = new GamePropertiesDialog(host_interface, parent); gpd->populate(ge); gpd->show(); gpd->onResize(); diff --git a/src/duckstation-qt/gamepropertiesdialog.h b/src/duckstation-qt/gamepropertiesdialog.h index fb2929bec..74c0def22 100644 --- a/src/duckstation-qt/gamepropertiesdialog.h +++ b/src/duckstation-qt/gamepropertiesdialog.h @@ -17,7 +17,7 @@ public: GamePropertiesDialog(QtHostInterface* host_interface, QWidget* parent = nullptr); ~GamePropertiesDialog(); - static void showForEntry(QtHostInterface* host_interface, const GameListEntry* ge); + static void showForEntry(QtHostInterface* host_interface, const GameListEntry* ge, QWidget* parent); public Q_SLOTS: void clear(); diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index e28c7d6ad..c600ec749 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -406,7 +406,7 @@ void MainWindow::onGameListContextMenuRequested(const QPoint& point, const GameL if (entry) { connect(menu.addAction(tr("Properties...")), &QAction::triggered, - [this, entry]() { GamePropertiesDialog::showForEntry(m_host_interface, entry); }); + [this, entry]() { GamePropertiesDialog::showForEntry(m_host_interface, entry, this); }); connect(menu.addAction(tr("Open Containing Directory...")), &QAction::triggered, [this, entry]() { const QFileInfo fi(QString::fromStdString(entry->path)); From 9f59b81277d8112459cdeeb23efa8ee1256baa39 Mon Sep 17 00:00:00 2001 From: Blackbird88 Date: Sat, 29 Aug 2020 15:47:48 +0200 Subject: [PATCH 29/61] More gameinis (#809) Co-authored-by: Connor McLaughlin --- data/database/gamesettings.ini | 39 +++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/data/database/gamesettings.ini b/data/database/gamesettings.ini index cb53a06a9..33fc1f299 100644 --- a/data/database/gamesettings.ini +++ b/data/database/gamesettings.ini @@ -14,7 +14,7 @@ ForcePGXPCPUMode = true # Doom (USA) (Rev 1) (SLUS-00077) [SLUS-00077] DisableUpscaling = true - +ForceDigitalController = true # Pop'n Music 6 (Japan) (SLPM-87089) [SLPM-87089] @@ -140,6 +140,43 @@ ForceDigitalController = true ForceDigitalController = true +# SCES-00344 (Crash Bandicoot (Europe)) +[SCES-00344] +ForceDigitalController = true + + +# SLUS-00355 (Duke Nukem - Total Meltdown (USA)) +[SLUS-00355] +DisableUpscaling = true +ForceDigitalController = true + + +# SLUS-00331 (Final Doom (USA)) +[SLUS-00331] +DisableUpscaling = true +ForceDigitalController = true + + +# SLUS-00106 (Grand Theft Auto (USA)) +[SLUS-00106] +ForceDigitalController = true + + +# SLUS-00005 (Rayman (USA)) +[SLUS-00005] +ForceDigitalController = true + + +# SLUS-01265 (Rayman Brain Games (USA)) +[SLUS-01265] +ForceDigitalController = true + + +# SLUS-00601 (Skullmonkeys (USA)) +[SLUS-00601] +ForceDigitalController = true + + # SLPS-00435 (Megatudo 2096 (Japan)) [SLPS-00435] ForceRecompilerICache = true From e35c2182e529452031e502a1788a30a8fa42927c Mon Sep 17 00:00:00 2001 From: Anderson_Cardoso <43047877+andercard0@users.noreply.github.com> Date: Sat, 29 Aug 2020 18:58:31 -0300 Subject: [PATCH 30/61] Update of Translation Translation Notes: Untranslatable Texts: - Port 1 | Port 2 Options in Controller Configuration; - Crosshair Image Path and Crosshair Image Scale (when choose Namco GunCon option); - Left and Right in Playstation Mouse binding options screen - Up, Down, Left, Right Steering option in NeoGcon options screen - Tabs: General, Graphics, Save States and Audio in Shortcut configuration screen - Save Game State 1 Load Game State 1~2.. etc. in Save States screen - Save Global State 1~9 in Save States screen - Keyboard/keypad+Minus and - Keyboard/keypad+Plus in Audio screen --- .../translations/duckstation-qt_pt-br.ts | 840 +++++++++++++++++- 1 file changed, 839 insertions(+), 1 deletion(-) diff --git a/src/duckstation-qt/translations/duckstation-qt_pt-br.ts b/src/duckstation-qt/translations/duckstation-qt_pt-br.ts index 4fc6adda8..9bf0b0c64 100644 --- a/src/duckstation-qt/translations/duckstation-qt_pt-br.ts +++ b/src/duckstation-qt/translations/duckstation-qt_pt-br.ts @@ -148,6 +148,178 @@ Permite o uso de dispositivos de depuração e shaders para renderizar APIs que os suportam. Só deve ser usado ao depurar o emulador. + + AnalogController + + + + Controller %u switched to analog mode. + Controle %u mudado para modo analogico. + + + + + Controller %u switched to digital mode. + Controle %u mudado para modo digital. + + + + Controller %u is locked to analog mode by the game. + Controle %u está travado em modo analogico pelo jogo. + + + + Controller %u is locked to digital mode by the game. + Controle %u está travado no modo digital pelo jogo. + + + + LeftX + Esquerda Eixo X + + + + LeftY + Esquerda Eixo Y + + + + RightX + Direita Eixo X + + + + RightY + Direita Eixo Y + + + + Up + 🠉 + + + + Down + 🠋 + + + + Left + 🠰 + 🠈 + + + + Right + ➡️ + + + + + Select + Select + + + + Start + Start + + + + Triangle + ⃤ 🛆⟁ + + + + + Cross + + + + + + Circle + + + + + Square + + + + + L1 + L1 + + + + L2 + L2 + + + + R1 + R1 + + + + R2 + R2 + + + + L3 + L3 + + + + R3 + R3 + + + + Analog + Analogico + + + + Enable Analog Mode on Reset + Ativar modo Analogico ao Reiniciar + + + + Automatically enables analog mode when the console is reset/powered on. + Ativa o modo analogico automaticamente quando o console é reiniciado / desligado. + + + + Analog Axis Scale + Escala de Eixo do Analogico + + + + Sets the analog stick axis scaling factor. A value between 1.30 and 1.40 is recommended when using recent controllers, e.g. DualShock 4, Xbox One Controller. + Seta a escala do eixo dos controles. Um valor entre 1.30 e 1.40 é recomendável quando estiver usando controles mais recentes, ex: Dualshock 4 e Controles de X-Box One. + + + + AudioBackend + + + Null (No Output) + Nulo (Sem som) + + + + Cubeb + Cubed + + + + SDL + + + AudioSettingsWidget @@ -359,6 +531,60 @@ Cancelar + + CPUExecutionMode + + + Intepreter (Slowest) + Interpretador (Mais Lento) + + + + Cached Interpreter (Faster) + Int. Armazenado (Rápido) + + + + Recompiler (Fastest) + Recompilador (Mais Rápido) + + + + CommonHostInterface + + + Are you sure you want to stop emulation? + Quer mesmo parar a Emulação? + + + + The current state will be saved. + O estado atual será salvo. + + + + ConsoleRegion + + + Auto-Detect + Auto Detectar + + + + NTSC-J (Japan) + NTSC-J (Japão) + + + + NTSC-U (US) + NTSC-U (US) + + + + PAL (Europe, Australia) + PAL (Europeu, Australia) + + ConsoleSettingsWidget @@ -458,6 +684,24 @@ Escolha o Arquivo de BIOS + + ControllerInterface + + + None + Nenhum + + + + SDL + SDL + + + + XInput + X-Input + + ControllerSettingsWidget @@ -567,6 +811,184 @@ Caminho não atribuido, configuração de controle não foi salva. + + ControllerType + + + None + Nenhum + + + + Digital Controller + Controle Digital + + + + Analog Controller (DualShock) + Controle Analogico (Dualshock) + + + + Namco GunCon + Namco GunCon + + + + PlayStation Mouse + Playstation Mouse + + + + NeGcon + NeGcon + + + + DigitalController + + + Up + 🠉 + 🠉 + + + + Down + ↓ ⭳ ⯆ ⮟ 🡇 🠋 + 🠋 + + + + Left + 🠰 + 🠈 + + + + Right + 🠊 🢧➜ ➡️ + + + + + Select + Select + + + + Start + Start + + + + Triangle + + 🛆 + + + + Cross + ╳Xx + + + + + Circle + + + + + + Square + ⃞ ⬛ ⬜ + + + + + L1 + L1 + + + + L2 + L2 + + + + R1 + R1 + + + + R2 + R2 + + + + DiscRegion + + + NTSC-J (Japan) + NTSC-J (Japão) + + + + NTSC-U (US) + NTSC-U (US) + + + + PAL (Europe, Australia) + PAL (Europeu, Australia) + + + + Other + Outros + + + + DisplayCropMode + + + None + Nenhum + + + + Only Overscan Area + Somente Área Renderizada + + + + All Borders + Todas as Bordas + + + + GPURenderer + + + Hardware (D3D11) + Placa de Video (D3D11) + + + + Hardware (Vulkan) + Placa de Video (Vulkan) + + + + Hardware (OpenGL) + Placa de Video (OpenGL) + + + + Software + Software + + GPUSettingsWidget @@ -967,6 +1389,39 @@ Padrão + + GameListCompatibilityRating + + + Unknown + Desconhecido + + + + Doesn't Boot + Não Funciona + + + + Crashes In Intro + Quebra logo no Inicio + + + + Crashes In-Game + Quebra durante o Jogo + + + + Graphical/Audio Issues + Problemas de Aúdio e Vídeo + + + + No Issues + Sem Problemas + + GameListModel @@ -1326,6 +1781,74 @@ This will download approximately 4 megabytes over your current internet connecti Dê ok para copiar para área de transferência. + + GameSettingsTrait + + + Force Interpreter + Forçar Interpretador + + + + Force Software Renderer + Forçar Renderização por Software + + + + Enable Interlacing + Ativar Entrelaçamento + + + + Disable True Color + Desativar Cor Real (True Color) + + + + Disable Upscaling + Desativar Escalonamento + + + + Disable Scaled Dithering + Desativar Escalonamento do Dithering + + + + Disable Widescreen + Desativar Func.Esticar (Widescreen) + + + + Disable PGXP + Desativar PGXP + + + + Disable PGXP Culling + Desativar Correção de Curvas + + + + Enable PGXP Vertex Cache + Ativar PGXP Vértice Armazenado + + + + Enable PGXP CPU Mode + Ativar PGXP - Modo CPU + + + + Force Digital Controller + Forçar Controle Digital (D-Pad) + + + + Enable Recompiler Memory Exceptions + Habilitar Exceções de Memória + + GeneralSettingsWidget @@ -1586,6 +2109,119 @@ This will download approximately 4 megabytes over your current internet connecti %1% + + Hotkeys + + + Fast Forward + Avanço Rápido + + + + Toggle Fast Forward + Pulo de Quadros (Alternado) + + + + Toggle Fullscreen + Tela Cheia + + + + Toggle Pause + Pausa + + + + Power Off System + Desligar o Sistema + + + + Save Screenshot + Salvar Caputra de tela + + + + Frame Step + Pulo de quadro (Fixo) + + + + Toggle Software Rendering + Alternar para Renderizador por Software + + + + Toggle PGXP + PGXP + + + + Increase Resolution Scale + Aumentar Escala de Resolução + + + + Decrease Resolution Scale + Diminuir Escala de Resolução + + + + Load From Selected Slot + Carregar do Estado Salvo + + + + Save To Selected Slot + Salvar para compartimento Selecionado + + + + Select Previous Save Slot + Selecionar compartimento anterior + + + + Select Next Save Slot + Selecionar próximo compartimento + + + + Load Game State %u + Carregar estado de jogo %u + + + + Save Game State %u + Salvar Estado do Jogo %u + + + + Load Global State %u + Carregar Estado Global %u + + + + Save Global State %u + Salvar Estado Global %u + + + + Toggle Mute + Mudo + + + + Volume Up + Volume + + + + + Volume Down + Volume - + + InputBindingDialog @@ -1639,6 +2275,59 @@ This will download approximately 4 megabytes over your current internet connecti Aperte Botão/Analogicos... [%1] + + LogLevel + + + None + Nenhum + + + + Error + Erro + + + + Warning + Atenção + + + + Performance + Performance + + + + Success + Sucesso + + + + Information + Informação + + + + Developer + Desenvolvedor + + + + Profile + Perfil + + + + Debug + Depurar + + + + Trace + Rastreio + + MainWindow @@ -1991,7 +2680,7 @@ This will download approximately 4 megabytes over your current internet connecti All File Types (*.bin *.img *.cue *.chd *.exe *.psexe *.psf);;Single-Track Raw Images (*.bin *.img);;Cue Sheets (*.cue);;MAME CHD Images (*.chd);;PlayStation Executables (*.exe *.psexe);;Portable Sound Format Files (*.psf);;Playlists (*.m3u) - + @@ -2134,6 +2823,122 @@ This will download approximately 4 megabytes over your current internet connecti Escolha o caminho para os Cartões de Memória + + MemoryCardType + + + No Memory Card + Sem Cartão de Memória + + + + Shared Between All Games + Compartrilhada Entre Jogos + + + + Separate Card Per Game (Game Code) + Separar Cartão Por Jogo (Cód. Jogo) + + + + Separate Card Per Game (Game Title) + Separar Cartão Por Jogo (Titulo. Jogo) + + + + NamcoGunCon + + + Trigger + Gatilho + + + + A + A + + + + B + B + + + + OSDMessage + + + System reset. + Sistema Reiniciado. + + + + Loading state from '%s'... + Carregando estado de '%s'... + + + + Loading state from '%s' failed. Resetting. + Carregamento de estado '%s'.falhou. Reiniciando. + + + + Saving state to '%s' failed. + Salvando estado para '%s' falhou. + + + + State saved to '%s'. + Estado salvo para '%s'. + + + + PGXP is incompatible with the software renderer, disabling PGXP. + PGXP é incompatível com o rederizador por software, desativando PGXP. + + + + PGXP CPU mode is incompatible with the recompiler, using Cached Interpreter instead. + PGXP em modo CPU não é compatível com o recompilador, mudando para Interpretador armazenado. + + + + Speed limiter enabled. + Limitador de Velocidade Ativado. + + + + Speed limiter disabled. + Limitador de Velocidade Desativado. + + + + Volume: Muted + Volume: Mudo + + + + + + Volume: %d%% + Volume: %d%% + + + + Loaded input profile from '%s' + Perfil de controle carregado de '%s' + + + + Failed to save screenshot to '%s' + Falha ao salvar captura para '%s' + + + + Screenshot saved to '%s'. + Captura de tela salva para '%s'. + + QObject @@ -2355,4 +3160,37 @@ The saves will not be recoverable. Recomendado + + System + + + Save state is incompatible: expecting version %u but state is version %u. + Estado salvo incompatível: versão do mesmo esperada %u não a versão %u. + + + + Failed to open CD image from save state: '%s'. + Falha ao abrir estado salvo: '%s'. + + + + Per-game memory card cannot be used for slot %u as the running game has no code. Using shared card instead. + Caminho para o Cartão de Memória no compartimento %u não pôde ser usado pois o jogo iniciado não possui um cód. válido. Será usado cartão compartilhado. + + + + Per-game memory card cannot be used for slot %u as the running game has no title. Using shared card instead. + Caminho para o Cartão de Memória no compartimento %u não pôde ser usado pois o jogo iniciado não possui um nome. válido. Será usado cartão compartilhado. + + + + Memory card path for slot %u is missing, using default. + Caminho para o Cartão de Memória %u incorreto, usando o padrão. + + + + Game changed, reloading memory cards. + Jogo trocado, recarregando Cartões de Memória. + + From 8d49c71ec10521335730e5c1a2c252e95e576169 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 30 Aug 2020 11:13:27 +1000 Subject: [PATCH 31/61] PGXP: Fix out-of-bounds write in hi/lo --- src/core/pgxp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/pgxp.cpp b/src/core/pgxp.cpp index 04ae11219..8fef9c7d9 100644 --- a/src/core/pgxp.cpp +++ b/src/core/pgxp.cpp @@ -142,8 +142,8 @@ static void PGXP_InitGTE(); // pgxp_cpu.h static void PGXP_InitCPU(); static PGXP_value CPU_reg_mem[34]; -#define CPU_Hi CPU_reg[33] -#define CPU_Lo CPU_reg[34] +#define CPU_Hi CPU_reg[32] +#define CPU_Lo CPU_reg[33] static PGXP_value CP0_reg_mem[32]; static PGXP_value* CPU_reg = CPU_reg_mem; From 1eac603c79c8e5c7ad92ca38a42d4b822cc5ad67 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 30 Aug 2020 11:15:02 +1000 Subject: [PATCH 32/61] CPU/Recompiler: AArch64 compile fix --- src/core/cpu_recompiler_code_generator_generic.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/cpu_recompiler_code_generator_generic.cpp b/src/core/cpu_recompiler_code_generator_generic.cpp index c9e9a7ee6..0acd1559f 100644 --- a/src/core/cpu_recompiler_code_generator_generic.cpp +++ b/src/core/cpu_recompiler_code_generator_generic.cpp @@ -1,4 +1,5 @@ #include "cpu_core.h" +#include "cpu_core_private.h" #include "cpu_recompiler_code_generator.h" namespace CPU::Recompiler { From cbbf599e4eac999fdaf46be8057fd56488f6581f Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 30 Aug 2020 15:34:08 +1000 Subject: [PATCH 33/61] Android: Multiple improvements - Save/resume state when closing/starting. - Error reporting - hopefully can figure out why it's not starting on some devices. - Reduce startup latency. - Add more options and descriptions to settings. --- android/.idea/misc.xml | 2 +- android/app/build.gradle | 4 + .../app/src/cpp/android_host_interface.cpp | 155 ++++++--- android/app/src/cpp/android_host_interface.h | 14 +- .../duckstation/AndroidHostInterface.java | 36 +- .../duckstation/EmulationActivity.java | 211 +++++++----- .../github/stenzek/duckstation/FileUtil.java | 7 +- .../stenzek/duckstation/GameListEntry.java | 18 +- .../stenzek/duckstation/MainActivity.java | 50 ++- .../TouchscreenControllerButtonView.java | 33 +- .../TouchscreenControllerView.java | 5 +- android/app/src/main/res/drawable/flag_eu.xml | 80 +++-- android/app/src/main/res/drawable/flag_jp.xml | 57 +++- android/app/src/main/res/drawable/flag_us.xml | 319 +++++++++++++++--- .../drawable/ic_baseline_play_arrow_24.xml | 6 +- .../drawable/ic_controller_circle_button.xml | 26 +- .../ic_controller_circle_button_pressed.xml | 26 +- .../drawable/ic_controller_cross_button.xml | 42 +-- .../ic_controller_cross_button_pressed.xml | 42 +-- .../drawable/ic_controller_down_button.xml | 16 +- .../ic_controller_down_button_pressed.xml | 16 +- .../res/drawable/ic_controller_l1_button.xml | 44 +-- .../ic_controller_l1_button_pressed.xml | 44 +-- .../res/drawable/ic_controller_l2_button.xml | 44 +-- .../ic_controller_l2_button_pressed.xml | 44 +-- .../drawable/ic_controller_left_button.xml | 16 +- .../ic_controller_left_button_pressed.xml | 16 +- .../res/drawable/ic_controller_r1_button.xml | 44 +-- .../ic_controller_r1_button_pressed.xml | 44 +-- .../res/drawable/ic_controller_r2_button.xml | 44 +-- .../ic_controller_r2_button_pressed.xml | 44 +-- .../drawable/ic_controller_right_button.xml | 16 +- .../ic_controller_right_button_pressed.xml | 16 +- .../drawable/ic_controller_select_button.xml | 16 +- .../ic_controller_select_button_pressed.xml | 16 +- .../drawable/ic_controller_square_button.xml | 28 +- .../ic_controller_square_button_pressed.xml | 28 +- .../drawable/ic_controller_start_button.xml | 16 +- .../ic_controller_start_button_pressed.xml | 16 +- .../ic_controller_triangle_button.xml | 26 +- .../ic_controller_triangle_button_pressed.xml | 26 +- .../res/drawable/ic_controller_up_button.xml | 16 +- .../ic_controller_up_button_pressed.xml | 16 +- .../main/res/drawable/ic_emblem_system.xml | 78 +++-- .../src/main/res/drawable/ic_media_cdrom.xml | 193 +++++------ .../app/src/main/res/layout/activity_main.xml | 28 +- .../app/src/main/res/layout/content_main.xml | 3 +- .../app/src/main/res/menu/menu_emulation.xml | 9 +- .../main/res/menu/menu_game_list_entry.xml | 4 +- android/app/src/main/res/menu/menu_main.xml | 22 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 4 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 4 +- android/app/src/main/res/values/arrays.xml | 32 +- android/app/src/main/res/values/strings.xml | 4 +- .../app/src/main/res/xml/root_preferences.xml | 255 +++++++------- 55 files changed, 1426 insertions(+), 995 deletions(-) diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml index 37a750962..7bfef59df 100644 --- a/android/.idea/misc.xml +++ b/android/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/android/app/build.gradle b/android/app/build.gradle index 782b0d479..d0b279d7c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,6 +24,10 @@ android { version "3.10.2" } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } defaultConfig { externalNativeBuild { cmake { diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index d13d6c205..682411a85 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -20,7 +20,14 @@ Log_SetChannel(AndroidHostInterface); static JavaVM* s_jvm; static jclass s_AndroidHostInterface_class; static jmethodID s_AndroidHostInterface_constructor; -static jfieldID s_AndroidHostInterface_field_nativePointer; +static jfieldID s_AndroidHostInterface_field_mNativePointer; +static jmethodID s_AndroidHostInterface_method_reportError; +static jmethodID s_AndroidHostInterface_method_reportMessage; +static jmethodID s_EmulationActivity_method_reportError; +static jmethodID s_EmulationActivity_method_reportMessage; +static jmethodID s_EmulationActivity_method_onEmulationStarted; +static jmethodID s_EmulationActivity_method_onEmulationStopped; +static jmethodID s_EmulationActivity_method_onGameTitleChanged; namespace AndroidHelpers { // helper for retrieving the current per-thread jni environment @@ -36,7 +43,7 @@ JNIEnv* GetJNIEnv() AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj) { return reinterpret_cast( - static_cast(env->GetLongField(obj, s_AndroidHostInterface_field_nativePointer))); + static_cast(env->GetLongField(obj, s_AndroidHostInterface_field_mNativePointer))); } std::string JStringToString(JNIEnv* env, jstring str) @@ -95,12 +102,26 @@ void AndroidHostInterface::RequestExit() void AndroidHostInterface::ReportError(const char* message) { - HostInterface::ReportError(message); + CommonHostInterface::ReportError(message); + + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + jstring message_jstr = env->NewStringUTF(message); + if (m_emulation_activity_object) + env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_reportError, message_jstr); + else + env->CallVoidMethod(m_java_object, s_AndroidHostInterface_method_reportError, message_jstr); } void AndroidHostInterface::ReportMessage(const char* message) { - HostInterface::ReportMessage(message); + CommonHostInterface::ReportMessage(message); + + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + jstring message_jstr = env->NewStringUTF(message); + if (m_emulation_activity_object) + env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_reportMessage, message_jstr); + else + env->CallVoidMethod(m_java_object, s_AndroidHostInterface_method_reportMessage, message_jstr); } std::string AndroidHostInterface::GetStringSettingValue(const char* section, const char* key, const char* default_value) @@ -141,22 +162,17 @@ void AndroidHostInterface::UpdateInputMap() CommonHostInterface::UpdateInputMap(m_settings_interface); } -bool AndroidHostInterface::StartEmulationThread(ANativeWindow* initial_surface, SystemBootParameters boot_params) +bool AndroidHostInterface::StartEmulationThread(jobject emulation_activity, ANativeWindow* initial_surface, + SystemBootParameters boot_params, bool resume_state) { Assert(!IsEmulationThreadRunning()); + emulation_activity = AndroidHelpers::GetJNIEnv()->NewGlobalRef(emulation_activity); + Log_DevPrintf("Starting emulation thread..."); m_emulation_thread_stop_request.store(false); - m_emulation_thread = - std::thread(&AndroidHostInterface::EmulationThreadEntryPoint, this, initial_surface, std::move(boot_params)); - m_emulation_thread_started.Wait(); - if (!m_emulation_thread_start_result.load()) - { - m_emulation_thread.join(); - Log_ErrorPrint("Failed to start emulation in thread"); - return false; - } - + m_emulation_thread = std::thread(&AndroidHostInterface::EmulationThreadEntryPoint, this, emulation_activity, + initial_surface, std::move(boot_params), resume_state); return true; } @@ -196,36 +212,48 @@ void AndroidHostInterface::RunOnEmulationThread(std::function function, m_callback_mutex.unlock(); } -void AndroidHostInterface::EmulationThreadEntryPoint(ANativeWindow* initial_surface, SystemBootParameters boot_params) +void AndroidHostInterface::EmulationThreadEntryPoint(jobject emulation_activity, ANativeWindow* initial_surface, + SystemBootParameters boot_params, bool resume_state) { JNIEnv* thread_env; if (s_jvm->AttachCurrentThread(&thread_env, nullptr) != JNI_OK) { - Log_ErrorPrintf("Failed to attach JNI to thread"); - m_emulation_thread_start_result.store(false); - m_emulation_thread_started.Signal(); + ReportError("Failed to attach JNI to thread"); return; } CreateImGuiContext(); m_surface = initial_surface; - ApplySettings(); + m_emulation_activity_object = emulation_activity; + ApplySettings(true); // Boot system. - if (!BootSystem(boot_params)) + bool boot_result = false; + if (resume_state) { - Log_ErrorPrintf("Failed to boot system on emulation thread (file:%s).", boot_params.filename.c_str()); + if (boot_params.filename.empty()) + boot_result = ResumeSystemFromMostRecentState(); + else + boot_result = ResumeSystemFromState(boot_params.filename.c_str(), true); + } + else + { + boot_result = BootSystem(boot_params); + } + + if (!boot_result) + { + ReportFormattedError("Failed to boot system on emulation thread (file:%s).", boot_params.filename.c_str()); DestroyImGuiContext(); - m_emulation_thread_start_result.store(false); - m_emulation_thread_started.Signal(); + thread_env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onEmulationStopped); + thread_env->DeleteGlobalRef(m_emulation_activity_object); + m_emulation_activity_object = {}; s_jvm->DetachCurrentThread(); return; } // System is ready to go. - m_emulation_thread_start_result.store(true); - m_emulation_thread_started.Signal(); - + thread_env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onEmulationStarted); while (!m_emulation_thread_stop_request.load()) { // run any events @@ -264,8 +292,11 @@ void AndroidHostInterface::EmulationThreadEntryPoint(ANativeWindow* initial_surf } } - DestroySystem(); + thread_env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onEmulationStopped); + PowerOffSystem(); DestroyImGuiContext(); + thread_env->DeleteGlobalRef(m_emulation_activity_object); + m_emulation_activity_object = {}; s_jvm->DetachCurrentThread(); } @@ -308,9 +339,27 @@ void AndroidHostInterface::ReleaseHostDisplay() m_display.reset(); } +void AndroidHostInterface::OnSystemDestroyed() +{ + CommonHostInterface::OnSystemDestroyed(); + ClearOSDMessages(); +} + +void AndroidHostInterface::OnRunningGameChanged() +{ + CommonHostInterface::OnRunningGameChanged(); + + if (m_emulation_activity_object) + { + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + jstring title_string = env->NewStringUTF(System::GetRunningTitle().c_str()); + env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onGameTitleChanged, title_string); + } +} + void AndroidHostInterface::SurfaceChanged(ANativeWindow* surface, int format, int width, int height) { - Log_InfoPrintf("SurfaceChanged %p %d %d %d", surface, format, width, height); + ReportFormattedMessage("SurfaceChanged %p %d %d %d", surface, format, width, height); if (m_surface == surface) { if (m_display) @@ -412,10 +461,11 @@ void AndroidHostInterface::RefreshGameList(bool invalidate_cache, bool invalidat m_game_list->Refresh(invalidate_cache, invalidate_database); } -void AndroidHostInterface::ApplySettings() +void AndroidHostInterface::ApplySettings(bool display_osd_messages) { Settings old_settings = std::move(g_settings); CommonHostInterface::LoadSettings(m_settings_interface); + CommonHostInterface::FixIncompatibleSettings(display_osd_messages); CheckForSettingsChanges(old_settings); } @@ -439,10 +489,26 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) return -1; } - if ((s_AndroidHostInterface_constructor = env->GetMethodID(s_AndroidHostInterface_class, "", "()V")) == - nullptr || - (s_AndroidHostInterface_field_nativePointer = - env->GetFieldID(s_AndroidHostInterface_class, "nativePointer", "J")) == nullptr) + jclass emulation_activity_class; + if ((s_AndroidHostInterface_constructor = + env->GetMethodID(s_AndroidHostInterface_class, "", "(Landroid/content/Context;)V")) == nullptr || + (s_AndroidHostInterface_field_mNativePointer = + env->GetFieldID(s_AndroidHostInterface_class, "mNativePointer", "J")) == nullptr || + (s_AndroidHostInterface_method_reportError = + env->GetMethodID(s_AndroidHostInterface_class, "reportError", "(Ljava/lang/String;)V")) == nullptr || + (s_AndroidHostInterface_method_reportMessage = + env->GetMethodID(s_AndroidHostInterface_class, "reportMessage", "(Ljava/lang/String;)V")) == nullptr || + (emulation_activity_class = env->FindClass("com/github/stenzek/duckstation/EmulationActivity")) == nullptr || + (s_EmulationActivity_method_reportError = + env->GetMethodID(emulation_activity_class, "reportError", "(Ljava/lang/String;)V")) == nullptr || + (s_EmulationActivity_method_reportMessage = + env->GetMethodID(emulation_activity_class, "reportMessage", "(Ljava/lang/String;)V")) == nullptr || + (s_EmulationActivity_method_onEmulationStarted = + env->GetMethodID(emulation_activity_class, "onEmulationStarted", "()V")) == nullptr || + (s_EmulationActivity_method_onEmulationStopped = + env->GetMethodID(emulation_activity_class, "onEmulationStopped", "()V")) == nullptr || + (s_EmulationActivity_method_onGameTitleChanged = + env->GetMethodID(emulation_activity_class, "onGameTitleChanged", "(Ljava/lang/String;)V")) == nullptr) { Log_ErrorPrint("AndroidHostInterface lookups failed"); return -1; @@ -457,12 +523,13 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) #define DEFINE_JNI_ARGS_METHOD(return_type, name, ...) \ extern "C" JNIEXPORT return_type JNICALL Java_com_github_stenzek_duckstation_##name(JNIEnv* env, __VA_ARGS__) -DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_create, jobject unused, jobject context_object, jstring user_directory) +DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_create, jobject unused, jobject context_object, + jstring user_directory) { Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEBUG); // initialize the java side - jobject java_obj = env->NewObject(s_AndroidHostInterface_class, s_AndroidHostInterface_constructor); + jobject java_obj = env->NewObject(s_AndroidHostInterface_class, s_AndroidHostInterface_constructor, context_object); if (!java_obj) { Log_ErrorPrint("Failed to create Java AndroidHostInterface"); @@ -483,7 +550,7 @@ DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_create, jobject unused, job return nullptr; } - env->SetLongField(java_obj, s_AndroidHostInterface_field_nativePointer, + env->SetLongField(java_obj, s_AndroidHostInterface_field_mNativePointer, static_cast(reinterpret_cast(cpp_obj))); return java_obj; @@ -494,8 +561,8 @@ DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_isEmulationThreadRunning, return AndroidHelpers::GetNativeClass(env, obj)->IsEmulationThreadRunning(); } -DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_startEmulationThread, jobject obj, jobject surface, - jstring filename, jstring state_filename) +DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_startEmulationThread, jobject obj, jobject emulationActivity, + jobject surface, jstring filename, jboolean resume_state, jstring state_filename) { ANativeWindow* native_surface = ANativeWindow_fromSurface(env, surface); if (!native_surface) @@ -509,7 +576,8 @@ DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_startEmulationThread, jobj SystemBootParameters boot_params; boot_params.filename = AndroidHelpers::JStringToString(env, filename); - return AndroidHelpers::GetNativeClass(env, obj)->StartEmulationThread(native_surface, std::move(boot_params)); + return AndroidHelpers::GetNativeClass(env, obj)->StartEmulationThread(emulationActivity, native_surface, + std::move(boot_params), resume_state); } DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_stopEmulationThread, jobject obj) @@ -526,7 +594,8 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_surfaceChanged, jobject obj, j AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); hi->RunOnEmulationThread( - [hi, native_surface, format, width, height]() { hi->SurfaceChanged(native_surface, format, width, height); }, true); + [hi, native_surface, format, width, height]() { hi->SurfaceChanged(native_surface, format, width, height); }, + false); } DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setControllerType, jobject obj, jint index, jstring controller_type) @@ -629,11 +698,11 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_applySettings, jobject obj) AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); if (hi->IsEmulationThreadRunning()) { - hi->RunOnEmulationThread([hi]() { hi->ApplySettings(); }); + hi->RunOnEmulationThread([hi]() { hi->ApplySettings(false); }); } else { - hi->ApplySettings(); + hi->ApplySettings(false); } } diff --git a/android/app/src/cpp/android_host_interface.h b/android/app/src/cpp/android_host_interface.h index fa72ca339..5667631b4 100644 --- a/android/app/src/cpp/android_host_interface.h +++ b/android/app/src/cpp/android_host_interface.h @@ -35,7 +35,8 @@ public: float GetFloatSettingValue(const char* section, const char* key, float default_value = 0.0f) override; bool IsEmulationThreadRunning() const { return m_emulation_thread.joinable(); } - bool StartEmulationThread(ANativeWindow* initial_surface, SystemBootParameters boot_params); + bool StartEmulationThread(jobject emulation_activity, ANativeWindow* initial_surface, + SystemBootParameters boot_params, bool resume_state); void RunOnEmulationThread(std::function function, bool blocking = false); void StopEmulationThread(); @@ -46,7 +47,7 @@ public: void SetControllerAxisState(u32 index, s32 button_code, float value); void RefreshGameList(bool invalidate_cache, bool invalidate_database); - void ApplySettings(); + void ApplySettings(bool display_osd_messages); protected: void SetUserDirectory() override; @@ -56,13 +57,18 @@ protected: bool AcquireHostDisplay() override; void ReleaseHostDisplay() override; + void OnSystemDestroyed() override; + void OnRunningGameChanged() override; + private: - void EmulationThreadEntryPoint(ANativeWindow* initial_surface, SystemBootParameters boot_params); + void EmulationThreadEntryPoint(jobject emulation_activity, ANativeWindow* initial_surface, + SystemBootParameters boot_params, bool resume_state); void CreateImGuiContext(); void DestroyImGuiContext(); jobject m_java_object = {}; + jobject m_emulation_activity_object = {}; AndroidSettingsInterface m_settings_interface; @@ -73,8 +79,6 @@ private: std::thread m_emulation_thread; std::atomic_bool m_emulation_thread_stop_request{false}; - std::atomic_bool m_emulation_thread_start_result{false}; - Common::Event m_emulation_thread_started; }; namespace AndroidHelpers { diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java index 59231f01e..87313d2a5 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java @@ -4,37 +4,57 @@ import android.content.Context; import android.os.Environment; import android.util.Log; import android.view.Surface; +import android.widget.Toast; -public class AndroidHostInterface -{ - private long nativePointer; +import com.google.android.material.snackbar.Snackbar; + +public class AndroidHostInterface { + private long mNativePointer; + private Context mContext; static public native AndroidHostInterface create(Context context, String userDirectory); - public AndroidHostInterface(long nativePointer) - { - this.nativePointer = nativePointer; + public AndroidHostInterface(Context context) { + this.mContext = context; + } + + public void reportError(String message) { + Toast.makeText(mContext, message, Toast.LENGTH_LONG).show(); + } + + public void reportMessage(String message) { + Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); } public native boolean isEmulationThreadRunning(); - public native boolean startEmulationThread(Surface surface, String filename, String state_filename); + + public native boolean startEmulationThread(EmulationActivity emulationActivity, Surface surface, String filename, boolean resumeState, String state_filename); + public native void stopEmulationThread(); public native void surfaceChanged(Surface surface, int format, int width, int height); // TODO: Find a better place for this. public native void setControllerType(int index, String typeName); + public native void setControllerButtonState(int index, int buttonCode, boolean pressed); + public native void setControllerAxisState(int index, int axisCode, float value); + public static native int getControllerButtonCode(String controllerType, String buttonName); + public static native int getControllerAxisCode(String controllerType, String axisName); public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase); + public native GameListEntry[] getGameListEntries(); public native void resetSystem(); + public native void loadState(boolean global, int slot); + public native void saveState(boolean global, int slot); + public native void applySettings(); static { @@ -42,6 +62,7 @@ public class AndroidHostInterface } static private AndroidHostInterface mInstance; + static public boolean createInstance(Context context) { // Set user path. String externalStorageDirectory = Environment.getExternalStorageDirectory().getAbsolutePath(); @@ -57,6 +78,7 @@ public class AndroidHostInterface static public boolean hasInstance() { return mInstance != null; } + static public AndroidHostInterface getInstance() { return mInstance; } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java index 8497145ec..66ba697f7 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java @@ -3,6 +3,7 @@ package com.github.stenzek.duckstation; import android.annotation.SuppressLint; import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; @@ -11,14 +12,12 @@ import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.Menu; -import android.view.MotionEvent; import android.view.SurfaceHolder; -import android.view.SurfaceView; import android.view.View; import android.view.MenuItem; import android.widget.FrameLayout; +import android.widget.Toast; -import androidx.core.app.NavUtils; import androidx.preference.PreferenceManager; /** @@ -30,77 +29,69 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde * Settings interfaces. */ SharedPreferences mPreferences; + private boolean getBooleanSetting(String key, boolean defaultValue) { return mPreferences.getBoolean(key, defaultValue); } + private void setBooleanSetting(String key, boolean value) { SharedPreferences.Editor editor = mPreferences.edit(); editor.putBoolean(key, value); editor.apply(); } + private String getStringSetting(String key, String defaultValue) { return mPreferences.getString(key, defaultValue); } - /** - * Touchscreen controller overlay - */ - TouchscreenControllerView mTouchscreenController; - private boolean mTouchscreenControllerVisible = true; + public void reportError(String message) { + Log.e("EmulationActivity", message); - /** - * Whether or not the system UI should be auto-hidden after - * {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds. - */ - private static final boolean AUTO_HIDE = true; + Object lock = new Object(); + runOnUiThread(() -> { + // Toast.makeText(this, message, Toast.LENGTH_LONG); + new AlertDialog.Builder(this) + .setTitle("Error") + .setMessage(message) + .setPositiveButton("OK", (dialog, button) -> { + dialog.dismiss(); + synchronized (lock) { + lock.notify(); + } + }) + .create() + .show(); + }); - /** - * If {@link #AUTO_HIDE} is set, the number of milliseconds to wait after - * user interaction before hiding the system UI. - */ - private static final int AUTO_HIDE_DELAY_MILLIS = 3000; - - /** - * Some older devices needs a small delay between UI widget updates - * and a change of the status and navigation bar. - */ - private static final int UI_ANIMATION_DELAY = 300; - private final Handler mHideHandler = new Handler(); - private EmulationSurfaceView mContentView; - private final Runnable mHidePart2Runnable = new Runnable() { - @SuppressLint("InlinedApi") - @Override - public void run() { - // Delayed removal of status and navigation bar - - // Note that some of these constants are new as of API 16 (Jelly Bean) - // and API 19 (KitKat). It is safe to use them, as they are inlined - // at compile-time and do nothing on earlier devices. - mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); - } - }; - private final Runnable mShowPart2Runnable = new Runnable() { - @Override - public void run() { - // Delayed display of UI elements - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.show(); + synchronized (lock) { + try { + lock.wait(); + } catch (InterruptedException e) { } } - }; - private boolean mVisible; - private final Runnable mHideRunnable = new Runnable() { - @Override - public void run() { - hide(); - } - }; + } + + public void reportMessage(String message) { + Log.i("EmulationActivity", message); + runOnUiThread(() -> { + Toast.makeText(this, message, Toast.LENGTH_SHORT); + }); + } + + public void onEmulationStarted() { + } + + public void onEmulationStopped() { + runOnUiThread(() -> { + finish(); + }); + } + + public void onGameTitleChanged(String title) { + runOnUiThread(() -> { + setTitle(title); + }); + } @Override public void surfaceCreated(SurfaceHolder holder) { @@ -114,16 +105,11 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde return; } - String bootPath = getIntent().getStringExtra("bootPath"); - String bootSaveStatePath = getIntent().getStringExtra("bootSaveStatePath"); - boolean resumeState = getIntent().getBooleanExtra("resumeState", false); + final String bootPath = getIntent().getStringExtra("bootPath"); + final boolean resumeState = getIntent().getBooleanExtra("resumeState", false); + final String bootSaveStatePath = getIntent().getStringExtra("saveStatePath"); - if (!AndroidHostInterface.getInstance() - .startEmulationThread(holder.getSurface(), bootPath, bootSaveStatePath)) { - Log.e("EmulationActivity", "Failed to start emulation thread"); - finishActivity(0); - return; - } + AndroidHostInterface.getInstance().startEmulationThread(this, holder.getSurface(), bootPath, resumeState, bootSaveStatePath); } @Override @@ -146,14 +132,14 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde actionBar.setDisplayHomeAsUpEnabled(true); } - mVisible = true; + mSystemUIVisible = true; mContentView = findViewById(R.id.fullscreen_content); mContentView.getHolder().addCallback(this); mContentView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - if (mVisible) - hide(); + if (mSystemUIVisible) + hideSystemUI(); } }); @@ -173,11 +159,16 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); + hideSystemUI(); + } - // Trigger the initial hide() shortly after the activity has been - // created, to briefly hint to the user that UI controls - // are available. - delayedHide(100); + @Override + protected void onStop() { + super.onStop(); + + if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) { + AndroidHostInterface.getInstance().stopEmulationThread(); + } } @Override @@ -228,37 +219,79 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde @Override public void onBackPressed() { - if (mVisible) { + if (mSystemUIVisible) { finish(); return; } - show(); + showSystemUI(); } - private void hide() { + /** + * Some older devices needs a small delay between UI widget updates + * and a change of the status and navigation bar. + */ + private static final int UI_ANIMATION_DELAY = 300; + private final Handler mSystemUIHideHandler = new Handler(); + private EmulationSurfaceView mContentView; + private final Runnable mHidePart2Runnable = new Runnable() { + @SuppressLint("InlinedApi") + @Override + public void run() { + // Delayed removal of status and navigation bar + + // Note that some of these constants are new as of API 16 (Jelly Bean) + // and API 19 (KitKat). It is safe to use them, as they are inlined + // at compile-time and do nothing on earlier devices. + mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + } + }; + private final Runnable mShowPart2Runnable = new Runnable() { + @Override + public void run() { + // Delayed display of UI elements + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.show(); + } + } + }; + private boolean mSystemUIVisible; + private final Runnable mHideRunnable = new Runnable() { + @Override + public void run() { + hideSystemUI(); + } + }; + + private void hideSystemUI() { // Hide UI first ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.hide(); } - mVisible = false; + mSystemUIVisible = false; // Schedule a runnable to remove the status and navigation bar after a delay - mHideHandler.removeCallbacks(mShowPart2Runnable); - mHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY); + mSystemUIHideHandler.removeCallbacks(mShowPart2Runnable); + mSystemUIHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY); } @SuppressLint("InlinedApi") - private void show() { + private void showSystemUI() { // Show the system bar mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); - mVisible = true; + mSystemUIVisible = true; // Schedule a runnable to display UI elements after a delay - mHideHandler.removeCallbacks(mHidePart2Runnable); - mHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY); + mSystemUIHideHandler.removeCallbacks(mHidePart2Runnable); + mSystemUIHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY); } /** @@ -266,10 +299,16 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde * previously scheduled calls. */ private void delayedHide(int delayMillis) { - mHideHandler.removeCallbacks(mHideRunnable); - mHideHandler.postDelayed(mHideRunnable, delayMillis); + mSystemUIHideHandler.removeCallbacks(mHideRunnable); + mSystemUIHideHandler.postDelayed(mHideRunnable, delayMillis); } + /** + * Touchscreen controller overlay + */ + TouchscreenControllerView mTouchscreenController; + private boolean mTouchscreenControllerVisible = true; + private void setTouchscreenControllerVisibility(boolean visible) { mTouchscreenControllerVisible = visible; mTouchscreenController.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java b/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java index 98825fd55..5f1a098c6 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java @@ -17,13 +17,13 @@ import java.lang.reflect.Array; import java.lang.reflect.Method; public final class FileUtil { - static String TAG="TAG"; + static String TAG = "TAG"; private static final String PRIMARY_VOLUME_NAME = "primary"; @Nullable public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) { if (treeUri == null) return null; - String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri),con); + String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con); if (volumePath == null) return File.separator; if (volumePath.endsWith(File.separator)) volumePath = volumePath.substring(0, volumePath.length() - 1); @@ -37,8 +37,7 @@ public final class FileUtil { return volumePath + documentPath; else return volumePath + File.separator + documentPath; - } - else return volumePath; + } else return volumePath; } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java index eb3fd8d63..e7d01a300 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java @@ -7,14 +7,12 @@ import android.widget.TextView; import androidx.core.content.ContextCompat; public class GameListEntry { - public enum EntryType - { + public enum EntryType { Disc, PSExe } - public enum CompatibilityRating - { + public enum CompatibilityRating { Unknown, DoesntBoot, CrashesInIntro, @@ -72,15 +70,21 @@ public class GameListEntry { return mTitle; } - public String getModifiedTime() { return mModifiedTime; } + public String getModifiedTime() { + return mModifiedTime; + } public DiscRegion getRegion() { return mRegion; } - public EntryType getType() { return mType; } + public EntryType getType() { + return mType; + } - public CompatibilityRating getCompatibilityRating() { return mCompatibilityRating; } + public CompatibilityRating getCompatibilityRating() { + return mCompatibilityRating; + } public void fillView(View view) { ((TextView) view.findViewById(R.id.game_list_view_entry_title)).setText(mTitle); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java index 95d2fe8cd..be2063b55 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java @@ -28,27 +28,26 @@ import android.view.MenuItem; import android.widget.AdapterView; import android.widget.ListView; import android.widget.PopupMenu; +import android.widget.Toast; import java.util.HashSet; import java.util.Set; import java.util.prefs.Preferences; +import static com.google.android.material.snackbar.Snackbar.make; + public class MainActivity extends AppCompatActivity { private static final int REQUEST_EXTERNAL_STORAGE_PERMISSIONS = 1; private static final int REQUEST_ADD_DIRECTORY_TO_GAME_LIST = 2; private GameList mGameList; private ListView mGameListView; + private boolean mHasExternalStoragePermissions = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (!AndroidHostInterface.hasInstance() && !AndroidHostInterface.createInstance(this)) { - Log.i("MainActivity", "Failed to create host interface"); - throw new RuntimeException("Failed to create host interface"); - } - setContentView(R.layout.activity_main); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); @@ -93,6 +92,18 @@ public class MainActivity extends AppCompatActivity { return true; } }); + + mHasExternalStoragePermissions = checkForExternalStoragePermissions(); + if (mHasExternalStoragePermissions) + completeStartup(); + } + + private void completeStartup() { + if (!AndroidHostInterface.hasInstance() && !AndroidHostInterface.createInstance(this)) { + Log.i("MainActivity", "Failed to create host interface"); + throw new RuntimeException("Failed to create host interface"); + } + mGameList.refresh(false, false); } @@ -122,12 +133,17 @@ public class MainActivity extends AppCompatActivity { int id = item.getItemId(); //noinspection SimplifiableIfStatement - if (id == R.id.action_add_game_directory) { + if (id == R.id.action_resume) { + startEmulation(null, true); + } else if (id == R.id.action_start_bios) { + startEmulation(null, false); + } else if (id == R.id.action_add_game_directory) { startAddGameDirectory(); } else if (id == R.id.action_scan_for_new_games) { mGameList.refresh(false, false); - } if (id == R.id.action_rescan_all_games) { - mGameList.refresh(true, false); + } + if (id == R.id.action_rescan_all_games) { + mGameList.refresh(true, true); } if (id == R.id.action_settings) { Intent intent = new Intent(this, SettingsActivity.class); @@ -190,19 +206,21 @@ public class MainActivity extends AppCompatActivity { int[] grantResults) { // check that all were successful for (int i = 0; i < grantResults.length; i++) { - if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { - Snackbar.make(mGameListView, - "External storage permissions are required to start emulation.", - Snackbar.LENGTH_LONG); + if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + if (!mHasExternalStoragePermissions) { + mHasExternalStoragePermissions = true; + completeStartup(); + } + } else { + Toast.makeText(this, + "External storage permissions are required to use DuckStation.", + Toast.LENGTH_LONG); + finish(); } } } private boolean startEmulation(String bootPath, boolean resumeState) { - if (!checkForExternalStoragePermissions()) { - return false; - } - Intent intent = new Intent(this, EmulationActivity.class); intent.putExtra("bootPath", bootPath); intent.putExtra("resumeState", resumeState); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerButtonView.java b/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerButtonView.java index 2ace2d51b..b5986e3e0 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerButtonView.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerButtonView.java @@ -19,25 +19,24 @@ public class TouchscreenControllerButtonView extends View { private String mButtonName = ""; private ButtonStateChangedListener mListener; - public interface ButtonStateChangedListener - { + public interface ButtonStateChangedListener { void onButtonStateChanged(TouchscreenControllerButtonView view, boolean pressed); } public TouchscreenControllerButtonView(Context context) { super(context); - init(context,null, 0); + init(context, null, 0); } public TouchscreenControllerButtonView(Context context, AttributeSet attrs) { super(context, attrs); - init(context,attrs, 0); + init(context, attrs, 0); } public TouchscreenControllerButtonView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - init(context,attrs, defStyle); + init(context, attrs, defStyle); } private void init(Context context, AttributeSet attrs, int defStyle) { @@ -80,15 +79,12 @@ public class TouchscreenControllerButtonView extends View { } @Override - public boolean onTouchEvent(MotionEvent event) - { + public boolean onTouchEvent(MotionEvent event) { final boolean oldState = mPressed; - switch (event.getAction()) - { + switch (event.getAction()) { case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - { + case MotionEvent.ACTION_POINTER_DOWN: { mPressed = true; invalidate(); @@ -99,8 +95,7 @@ public class TouchscreenControllerButtonView extends View { } case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - { + case MotionEvent.ACTION_POINTER_UP: { mPressed = false; invalidate(); @@ -114,8 +109,7 @@ public class TouchscreenControllerButtonView extends View { return super.onTouchEvent(event); } - public boolean isPressed() - { + public boolean isPressed() { return mPressed; } @@ -127,13 +121,11 @@ public class TouchscreenControllerButtonView extends View { mButtonName = buttonName; } - public int getButtonCode() - { + public int getButtonCode() { return mButtonCode; } - public void setButtonCode(int code) - { + public void setButtonCode(int code) { mButtonCode = code; } @@ -153,8 +145,7 @@ public class TouchscreenControllerButtonView extends View { mUnpressedDrawable = unpressedDrawable; } - public void setButtonStateChangedListener(ButtonStateChangedListener listener) - { + public void setButtonStateChangedListener(ButtonStateChangedListener listener) { mListener = listener; } } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java b/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java index 0c43b379e..1b7f475c4 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java @@ -50,9 +50,8 @@ public class TouchscreenControllerView extends FrameLayout implements Touchscree linkButton(view, R.id.controller_button_r2, "R2"); } - private void linkButton(View view, int id, String buttonName) - { - TouchscreenControllerButtonView buttonView = (TouchscreenControllerButtonView)view.findViewById(id); + private void linkButton(View view, int id, String buttonName) { + TouchscreenControllerButtonView buttonView = (TouchscreenControllerButtonView) view.findViewById(id); buttonView.setButtonName(buttonName); buttonView.setButtonStateChangedListener(this); diff --git a/android/app/src/main/res/drawable/flag_eu.xml b/android/app/src/main/res/drawable/flag_eu.xml index a3bf3c7f0..e545a72a2 100644 --- a/android/app/src/main/res/drawable/flag_eu.xml +++ b/android/app/src/main/res/drawable/flag_eu.xml @@ -1,36 +1,70 @@ - - - - + + + - - - + + + - + - - - + + + - + android:strokeColor="#00000000" + android:strokeWidth="1"> - - - + + + diff --git a/android/app/src/main/res/drawable/flag_jp.xml b/android/app/src/main/res/drawable/flag_jp.xml index 8ff9d256f..f7b9fe6bd 100644 --- a/android/app/src/main/res/drawable/flag_jp.xml +++ b/android/app/src/main/res/drawable/flag_jp.xml @@ -1,26 +1,49 @@ - - - - + + + - - - + + + - + android:strokeColor="#00000000" + android:strokeWidth="1"> - - - + + + diff --git a/android/app/src/main/res/drawable/flag_us.xml b/android/app/src/main/res/drawable/flag_us.xml index 08c798d33..d83fcb9e2 100644 --- a/android/app/src/main/res/drawable/flag_us.xml +++ b/android/app/src/main/res/drawable/flag_us.xml @@ -1,5 +1,4 @@ - - + - + - + - + - + - + - + @@ -81,32 +104,56 @@ - + - + - + - + - + - + @@ -115,32 +162,56 @@ - + - + - + - + - + - + @@ -149,32 +220,56 @@ - + - + - + - + - + - + @@ -183,32 +278,56 @@ - + - + - + - + - + - + @@ -217,112 +336,200 @@ - - + + - + - + - + - + - - + + - + - + - + - + - - + + - + - + - + - + - - + + - + - + - + - + diff --git a/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml b/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml index 13c137a92..41f4a52ed 100644 --- a/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml +++ b/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml @@ -4,7 +4,7 @@ android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_circle_button.xml b/android/app/src/main/res/drawable/ic_controller_circle_button.xml index c078d372c..d9fa460c6 100644 --- a/android/app/src/main/res/drawable/ic_controller_circle_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_circle_button.xml @@ -3,17 +3,17 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.45833"> - - + + diff --git a/android/app/src/main/res/drawable/ic_controller_circle_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_circle_button_pressed.xml index a68b83f98..47953d0c1 100644 --- a/android/app/src/main/res/drawable/ic_controller_circle_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_circle_button_pressed.xml @@ -3,17 +3,17 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.45833"> - - + + diff --git a/android/app/src/main/res/drawable/ic_controller_cross_button.xml b/android/app/src/main/res/drawable/ic_controller_cross_button.xml index c0b10a80a..b133bd616 100644 --- a/android/app/src/main/res/drawable/ic_controller_cross_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_cross_button.xml @@ -3,25 +3,25 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.45833"> - - - + + + diff --git a/android/app/src/main/res/drawable/ic_controller_cross_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_cross_button_pressed.xml index 9f1766ec1..b00988473 100644 --- a/android/app/src/main/res/drawable/ic_controller_cross_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_cross_button_pressed.xml @@ -3,25 +3,25 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.45833"> - - - + + + diff --git a/android/app/src/main/res/drawable/ic_controller_down_button.xml b/android/app/src/main/res/drawable/ic_controller_down_button.xml index b397fd762..898f2c54f 100644 --- a/android/app/src/main/res/drawable/ic_controller_down_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_down_button.xml @@ -3,12 +3,12 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.458332"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_down_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_down_button_pressed.xml index ecc4d7ae7..4e7d5a35e 100644 --- a/android/app/src/main/res/drawable/ic_controller_down_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_down_button_pressed.xml @@ -3,12 +3,12 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.458332"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_l1_button.xml b/android/app/src/main/res/drawable/ic_controller_l1_button.xml index 0bc81724b..9f3ab7240 100644 --- a/android/app/src/main/res/drawable/ic_controller_l1_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_l1_button.xml @@ -3,26 +3,26 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - - - + + + diff --git a/android/app/src/main/res/drawable/ic_controller_l1_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_l1_button_pressed.xml index 8c06f6467..06d625fe5 100644 --- a/android/app/src/main/res/drawable/ic_controller_l1_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_l1_button_pressed.xml @@ -3,26 +3,26 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - - - + + + diff --git a/android/app/src/main/res/drawable/ic_controller_l2_button.xml b/android/app/src/main/res/drawable/ic_controller_l2_button.xml index 3ca495405..3853d103f 100644 --- a/android/app/src/main/res/drawable/ic_controller_l2_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_l2_button.xml @@ -3,26 +3,26 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - - - + + + diff --git a/android/app/src/main/res/drawable/ic_controller_l2_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_l2_button_pressed.xml index 05fd394f7..eeaefb18b 100644 --- a/android/app/src/main/res/drawable/ic_controller_l2_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_l2_button_pressed.xml @@ -3,26 +3,26 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - - - + + + diff --git a/android/app/src/main/res/drawable/ic_controller_left_button.xml b/android/app/src/main/res/drawable/ic_controller_left_button.xml index 0dd3138b1..ccd46c3cc 100644 --- a/android/app/src/main/res/drawable/ic_controller_left_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_left_button.xml @@ -3,12 +3,12 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.458332"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_left_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_left_button_pressed.xml index 11907981a..39c1de9e1 100644 --- a/android/app/src/main/res/drawable/ic_controller_left_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_left_button_pressed.xml @@ -3,12 +3,12 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.458332"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_r1_button.xml b/android/app/src/main/res/drawable/ic_controller_r1_button.xml index a95517436..3130def38 100644 --- a/android/app/src/main/res/drawable/ic_controller_r1_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_r1_button.xml @@ -3,26 +3,26 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - - - + + + diff --git a/android/app/src/main/res/drawable/ic_controller_r1_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_r1_button_pressed.xml index 05fe52148..352ddbc6a 100644 --- a/android/app/src/main/res/drawable/ic_controller_r1_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_r1_button_pressed.xml @@ -3,26 +3,26 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - - - + + + diff --git a/android/app/src/main/res/drawable/ic_controller_r2_button.xml b/android/app/src/main/res/drawable/ic_controller_r2_button.xml index 251b747f9..195fbe85d 100644 --- a/android/app/src/main/res/drawable/ic_controller_r2_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_r2_button.xml @@ -3,26 +3,26 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - - - + + + diff --git a/android/app/src/main/res/drawable/ic_controller_r2_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_r2_button_pressed.xml index c5fe48ef6..640c36863 100644 --- a/android/app/src/main/res/drawable/ic_controller_r2_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_r2_button_pressed.xml @@ -3,26 +3,26 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - - - + + + diff --git a/android/app/src/main/res/drawable/ic_controller_right_button.xml b/android/app/src/main/res/drawable/ic_controller_right_button.xml index 586a11b79..8545a61ce 100644 --- a/android/app/src/main/res/drawable/ic_controller_right_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_right_button.xml @@ -3,12 +3,12 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.458332"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_right_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_right_button_pressed.xml index fabb570ae..f0cff05ba 100644 --- a/android/app/src/main/res/drawable/ic_controller_right_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_right_button_pressed.xml @@ -3,12 +3,12 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.458332"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_select_button.xml b/android/app/src/main/res/drawable/ic_controller_select_button.xml index ec850fc8a..bea56389c 100644 --- a/android/app/src/main/res/drawable/ic_controller_select_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_select_button.xml @@ -3,12 +3,12 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_select_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_select_button_pressed.xml index 1f5bd9ca2..a34a925d4 100644 --- a/android/app/src/main/res/drawable/ic_controller_select_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_select_button_pressed.xml @@ -3,12 +3,12 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_square_button.xml b/android/app/src/main/res/drawable/ic_controller_square_button.xml index 9ce49e148..0da658c01 100644 --- a/android/app/src/main/res/drawable/ic_controller_square_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_square_button.xml @@ -3,18 +3,18 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.45833"> - - + + diff --git a/android/app/src/main/res/drawable/ic_controller_square_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_square_button_pressed.xml index 731d2eed9..248a1d400 100644 --- a/android/app/src/main/res/drawable/ic_controller_square_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_square_button_pressed.xml @@ -3,18 +3,18 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.45833"> - - + + diff --git a/android/app/src/main/res/drawable/ic_controller_start_button.xml b/android/app/src/main/res/drawable/ic_controller_start_button.xml index e7e5d8ba7..7247bbcd7 100644 --- a/android/app/src/main/res/drawable/ic_controller_start_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_start_button.xml @@ -3,12 +3,12 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_start_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_start_button_pressed.xml index 85f176935..5a8255577 100644 --- a/android/app/src/main/res/drawable/ic_controller_start_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_start_button_pressed.xml @@ -3,12 +3,12 @@ android:height="50dp" android:viewportWidth="26.458332" android:viewportHeight="13.229165"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_triangle_button.xml b/android/app/src/main/res/drawable/ic_controller_triangle_button.xml index 7645a89e7..9a1392988 100644 --- a/android/app/src/main/res/drawable/ic_controller_triangle_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_triangle_button.xml @@ -3,17 +3,17 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.45833"> - - + + diff --git a/android/app/src/main/res/drawable/ic_controller_triangle_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_triangle_button_pressed.xml index 0bab1c10a..be10c7a85 100644 --- a/android/app/src/main/res/drawable/ic_controller_triangle_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_triangle_button_pressed.xml @@ -3,17 +3,17 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.45833"> - - + + diff --git a/android/app/src/main/res/drawable/ic_controller_up_button.xml b/android/app/src/main/res/drawable/ic_controller_up_button.xml index ad440f992..e8b5a4064 100644 --- a/android/app/src/main/res/drawable/ic_controller_up_button.xml +++ b/android/app/src/main/res/drawable/ic_controller_up_button.xml @@ -3,12 +3,12 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.458332"> - + diff --git a/android/app/src/main/res/drawable/ic_controller_up_button_pressed.xml b/android/app/src/main/res/drawable/ic_controller_up_button_pressed.xml index 416454f5a..d17013321 100644 --- a/android/app/src/main/res/drawable/ic_controller_up_button_pressed.xml +++ b/android/app/src/main/res/drawable/ic_controller_up_button_pressed.xml @@ -3,12 +3,12 @@ android:height="100dp" android:viewportWidth="26.458332" android:viewportHeight="26.458332"> - + diff --git a/android/app/src/main/res/drawable/ic_emblem_system.xml b/android/app/src/main/res/drawable/ic_emblem_system.xml index 32b3d2397..4be483cfc 100644 --- a/android/app/src/main/res/drawable/ic_emblem_system.xml +++ b/android/app/src/main/res/drawable/ic_emblem_system.xml @@ -4,44 +4,42 @@ android:height="48dp" android:viewportWidth="48" android:viewportHeight="48"> - - - - - - + + + + diff --git a/android/app/src/main/res/drawable/ic_media_cdrom.xml b/android/app/src/main/res/drawable/ic_media_cdrom.xml index 74afde8b9..9269ea16f 100644 --- a/android/app/src/main/res/drawable/ic_media_cdrom.xml +++ b/android/app/src/main/res/drawable/ic_media_cdrom.xml @@ -4,107 +4,94 @@ android:height="48dp" android:viewportWidth="48" android:viewportHeight="48"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index 963330c6e..a034d7c7e 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,5 @@ - - + android:layout_height="match_parent" + android:orientation="horizontal"> + + + + + + - + + app:layout_constraintTop_toTopOf="parent" /> \ No newline at end of file diff --git a/android/app/src/main/res/menu/menu_emulation.xml b/android/app/src/main/res/menu/menu_emulation.xml index 439e736a5..c57b01b38 100644 --- a/android/app/src/main/res/menu/menu_emulation.xml +++ b/android/app/src/main/res/menu/menu_emulation.xml @@ -3,11 +3,14 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> - - - diff --git a/android/app/src/main/res/menu/menu_game_list_entry.xml b/android/app/src/main/res/menu/menu_game_list_entry.xml index 45bbbb8c6..1fb01d997 100644 --- a/android/app/src/main/res/menu/menu_game_list_entry.xml +++ b/android/app/src/main/res/menu/menu_game_list_entry.xml @@ -13,8 +13,8 @@ android:title="Slow Boot" /> - + android:title="Load State"> + diff --git a/android/app/src/main/res/menu/menu_main.xml b/android/app/src/main/res/menu/menu_main.xml index dc942bbfd..9ad26feca 100644 --- a/android/app/src/main/res/menu/menu_main.xml +++ b/android/app/src/main/res/menu/menu_main.xml @@ -1,16 +1,28 @@ + tools:context="com.github.stenzek.duckstation.MainActivity"> + + + + - - - - diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 036d09bc5..c9ad5f98f 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 036d09bc5..c9ad5f98f 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml index 66fbe92dc..c35f0db98 100644 --- a/android/app/src/main/res/values/arrays.xml +++ b/android/app/src/main/res/values/arrays.xml @@ -32,22 +32,22 @@ Software - 1x (1024x512 VRAM) - 2x (2048x1024 VRAM) - 3x (3072x1536 VRAM) - 4x (4096x2048 VRAM) - 5x (5120x2560 VRAM) - 6x (6144x3072 VRAM) - 7x (7168x3584 VRAM) - 8x (8192x4096 VRAM) - 9x (9216x4608 VRAM) - 10x (10240x5120 VRAM) - 11x (11264x5632 VRAM) - 12x (12288x6144 VRAM) - 13x (13312x6656 VRAM) - 14x (14336x7168 VRAM) - 15x (15360x7680 VRAM) - 16x (16384x8192 VRAM) + 1x + 2x + 3x (for 720p) + 4x + 5x (for 1080p) + 6x (for 1440p) + 7x + 8x + 9x (for 4K) + 10x + 11x + 12x + 13x + 14x + 15x + 16x 1 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 4e41aacef..b849520af 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -30,11 +30,11 @@ Show VPS - Execution Mode + CPU Execution Mode Interpreter - Renderer + GPU Renderer Display Linear Filtering Resolution Scale True 24-Bit Color (Disables Dithering) diff --git a/android/app/src/main/res/xml/root_preferences.xml b/android/app/src/main/res/xml/root_preferences.xml index 6589670a5..c4fcb9e17 100644 --- a/android/app/src/main/res/xml/root_preferences.xml +++ b/android/app/src/main/res/xml/root_preferences.xml @@ -17,18 +17,67 @@ - - - - + + + + + + + + + + + + + + + + @@ -41,79 +90,15 @@ app:defaultValue="@string/settings_console_region_default" app:useSimpleSummaryProvider="true" /> - - + app:summary="Skips the BIOS shell/intro, booting directly into the game. Usually safe to enable, but some games break." /> - - - - - - - - - - - - - - - - - - - - + - - - - - - + app:summary="Forces the precision of colours output to the console's framebuffer to use the full 8 bits of precision per channel. This produces nicer looking gradients at the cost of making some colours look slightly different. Disabling the option also enables dithering, which makes the transition between colours less sharp by applying a pattern around those pixels. Most games are compatible with this option, but there is a number which aren't and will have broken effects with it enabled. Only applies to the hardware renderers." /> + app:key="GPU/ScaledDithering" + app:title="Scaled Dithering (scale dither pattern to resolution)" + app:defaultValue="true" + app:summary="Scales the dither pattern to the resolution scale of the emulated GPU. This makes the dither pattern much less obvious at higher resolutions. Usually safe to enable, and only supported by the hardware renderers." /> - - + app:key="GPU/DisableInterlacing" + app:title="Disable Interlacing (force progressive render/scan)" + app:defaultValue="true" + app:summary="Forces the rendering and display of frames to progressive mode. This removes the "combing" effect seen in 480i games by rendering them in 480p. Usually safe to enable." /> + + + + + + + app:defaultValue="false" + app:summary="Reduces "wobbly" polygons and "warping" textures that are common in PS1 games. >Only works with the hardware renderers. May not be compatible with all games." /> - + app:defaultValue="true" + app:summary="Increases the precision of polygon culling, reducing the number of holes in geometry. Requires geometry correction enabled." /> + - - + app:summary="Uses perspective-correct interpolation for texture coordinates and colors, straightening out warped textures. Requires geometry correction enabled." /> - + - - - - - - - + + - - + app:defaultValue="false" + app:summary="Adds padding to the display area to ensure that the ratio between pixels on the host to pixels in the console is an integer number. May result in a sharper image in some 2D games." /> + @@ -224,7 +209,7 @@ app:title="Display Touchscreen Controller" app:defaultValue="true" /> - + + + + + + + + From 32410b4254f1daf32258a29098db7493c411d150 Mon Sep 17 00:00:00 2001 From: Sam Pearman Date: Sun, 30 Aug 2020 22:47:59 +0900 Subject: [PATCH 34/61] Translation assistance scripts Translation assistance tool for quick and easy file generation/update/edit for future translators. --- .../translations/set-language.bat | 24 +++++++++++++++++++ .../translations/update-and-edit-language.bat | 12 ++++++++++ 2 files changed, 36 insertions(+) create mode 100644 src/duckstation-qt/translations/set-language.bat create mode 100644 src/duckstation-qt/translations/update-and-edit-language.bat diff --git a/src/duckstation-qt/translations/set-language.bat b/src/duckstation-qt/translations/set-language.bat new file mode 100644 index 000000000..b1a0c3694 --- /dev/null +++ b/src/duckstation-qt/translations/set-language.bat @@ -0,0 +1,24 @@ +@echo off +echo Set your language +echo. +echo Examples: +echo en ^<-- English +echo en-au ^<-- Australian English +echo ^<-- Remove language setting +echo. +echo For the 369-1 2-digit language code +echo https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes +echo. +echo If you require a country code as well (you probably don't) +echo https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes +echo. +echo.%lang% +set /p newlang="Enter language code: " +if defined newlang ( setx lang %newlang% ) +if defined lang if not defined newlang ( + echo Removing language setting... + setx lang "" 1>nul + reg delete HKCU\Environment /F /V lang 2>nul + reg delete "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /F /V lang 2>nul +) +pause diff --git a/src/duckstation-qt/translations/update-and-edit-language.bat b/src/duckstation-qt/translations/update-and-edit-language.bat new file mode 100644 index 000000000..7931b953a --- /dev/null +++ b/src/duckstation-qt/translations/update-and-edit-language.bat @@ -0,0 +1,12 @@ +@echo off + +if not defined lang (echo Please set your language first & pause & exit) + +set "linguist=..\..\..\dep\msvc\qt\5.15.0\msvc2017_64\bin" +set context=..\ + +"%linguist%\lupdate.exe" %context% -ts duckstation-qt_%lang%.ts +pause + +cd "%linguist%" +start /B linguist.exe "%~dp0\duckstation-qt_%lang%.ts" From f187ee49712df1a921cbce6b26ea4bd91959a5bb Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Mon, 31 Aug 2020 20:54:59 +1000 Subject: [PATCH 35/61] TimingEvents: Switch from heap to sorted linked list --- src/core/timing_event.cpp | 278 +++++++++++++++++++++++++++----------- src/core/timing_event.h | 3 + 2 files changed, 205 insertions(+), 76 deletions(-) diff --git a/src/core/timing_event.cpp b/src/core/timing_event.cpp index 42f7edfc9..1580aa188 100644 --- a/src/core/timing_event.cpp +++ b/src/core/timing_event.cpp @@ -8,11 +8,12 @@ Log_SetChannel(TimingEvents); namespace TimingEvents { -static std::vector s_events; +static TimingEvent* s_active_events_head; +static TimingEvent* s_active_events_tail; +static TimingEvent* s_current_event = nullptr; +static u32 s_active_event_count = 0; static u32 s_global_tick_counter = 0; static u32 s_last_event_run_time = 0; -static bool s_running_events = false; -static bool s_events_need_sorting = false; u32 GetGlobalTickCounter() { @@ -32,7 +33,7 @@ void Reset() void Shutdown() { - Assert(s_events.empty()); + Assert(s_active_event_count == 0); } std::unique_ptr CreateTimingEvent(std::string name, TickCount period, TickCount interval, @@ -49,126 +50,248 @@ std::unique_ptr CreateTimingEvent(std::string name, TickCount perio void UpdateCPUDowncount() { if (!CPU::g_state.frame_done) - CPU::g_state.downcount = s_events[0]->GetDowncount(); + CPU::g_state.downcount = s_active_events_head->GetDowncount(); } -static bool CompareEvents(const TimingEvent* lhs, const TimingEvent* rhs) +static void SortEvent(TimingEvent* event) { - return lhs->GetDowncount() > rhs->GetDowncount(); + const TickCount event_downcount = event->m_downcount; + + if (event->prev && event->prev->m_downcount > event_downcount) + { + // move backwards + TimingEvent* current = event->prev; + while (current && current->m_downcount > event_downcount) + current = current->prev; + + // unlink + if (event->prev) + event->prev->next = event->next; + else + s_active_events_head = event->next; + if (event->next) + event->next->prev = event->prev; + else + s_active_events_tail = event->prev; + + // insert after current + if (current) + { + event->next = current->next; + if (current->next) + current->next->prev = event; + else + s_active_events_tail = event; + + event->prev = current; + current->next = event; + } + else + { + // insert at front + DebugAssert(s_active_events_head); + s_active_events_head->prev = event; + event->prev = nullptr; + event->next = s_active_events_head; + s_active_events_head = event; + UpdateCPUDowncount(); + } + } + else if (event->next && event_downcount > event->next->m_downcount) + { + // move forwards + TimingEvent* current = event->next; + while (current && event_downcount > current->m_downcount) + current = current->next; + + // unlink + if (event->prev) + event->prev->next = event->next; + else + s_active_events_head = event->next; + if (event->next) + event->next->prev = event->prev; + else + s_active_events_tail = event->prev; + + // insert before current + if (current) + { + event->next = current; + event->prev = current->prev; + + if (current->prev) + current->prev->next = event; + else + s_active_events_head = event; + + current->prev = event; + } + else + { + // insert at back + DebugAssert(s_active_events_tail); + s_active_events_tail->next = event; + event->next = nullptr; + event->prev = s_active_events_tail; + s_active_events_tail = event; + } + } } static void AddActiveEvent(TimingEvent* event) { - s_events.push_back(event); - if (!s_running_events) + DebugAssert(!event->prev && !event->next); + s_active_event_count++; + + TimingEvent* current = nullptr; + TimingEvent* next = s_active_events_head; + while (next && event->m_downcount > next->m_downcount) { - std::push_heap(s_events.begin(), s_events.end(), CompareEvents); + current = next; + next = next->next; + } + + if (!next) + { + // new tail + event->prev = s_active_events_tail; + if (s_active_events_tail) + { + s_active_events_tail->next = event; + s_active_events_tail = event; + } + else + { + // first event + s_active_events_tail = event; + s_active_events_head = event; + UpdateCPUDowncount(); + } + } + else if (!current) + { + // new head + event->next = s_active_events_head; + s_active_events_head->prev = event; + s_active_events_head = event; UpdateCPUDowncount(); } else { - s_events_need_sorting = true; + // inbetween current < event > next + event->prev = current; + event->next = next; + current->next = event; + next->prev = event; } } static void RemoveActiveEvent(TimingEvent* event) { - auto iter = std::find_if(s_events.begin(), s_events.end(), [event](const auto& it) { return event == it; }); - if (iter == s_events.end()) - { - Panic("Attempt to remove inactive event"); - return; - } + DebugAssert(s_active_event_count > 0); - s_events.erase(iter); - if (!s_running_events) + if (event->next) { - std::make_heap(s_events.begin(), s_events.end(), CompareEvents); - if (!s_events.empty()) - UpdateCPUDowncount(); + event->next->prev = event->prev; } else { - s_events_need_sorting = true; + s_active_events_tail = event->prev; } -} -static TimingEvent* FindActiveEvent(const char* name) -{ - auto iter = - std::find_if(s_events.begin(), s_events.end(), [&name](auto& ev) { return ev->GetName().compare(name) == 0; }); + if (event->prev) + { + event->prev->next = event->next; + } + else + { + s_active_events_head = event->next; + UpdateCPUDowncount(); + } - return (iter != s_events.end()) ? *iter : nullptr; + event->prev = nullptr; + event->next = nullptr; + + s_active_event_count--; } static void SortEvents() { - if (!s_running_events) + std::vector events; + events.reserve(s_active_event_count); + + TimingEvent* next = s_active_events_head; + while (next) { - std::make_heap(s_events.begin(), s_events.end(), CompareEvents); - UpdateCPUDowncount(); + TimingEvent* current = next; + events.push_back(current); + next = current->next; + current->prev = nullptr; + current->next = nullptr; } - else + + s_active_events_head = nullptr; + s_active_events_tail = nullptr; + s_active_event_count = 0; + + for (TimingEvent* event : events) + AddActiveEvent(event); +} + +static TimingEvent* FindActiveEvent(const char* name) +{ + for (TimingEvent* event = s_active_events_head; event; event = event->next) { - s_events_need_sorting = true; + if (event->GetName().compare(name) == 0) + return event; } + + return nullptr; } void RunEvents() { - DebugAssert(!s_running_events && !s_events.empty()); - - s_running_events = true; + DebugAssert(!s_current_event); TickCount pending_ticks = (s_global_tick_counter + CPU::GetPendingTicks()) - s_last_event_run_time; CPU::ResetPendingTicks(); while (pending_ticks > 0) { - const TickCount time = std::min(pending_ticks, s_events[0]->GetDowncount()); + const TickCount time = std::min(pending_ticks, s_active_events_head->GetDowncount()); s_global_tick_counter += static_cast(time); pending_ticks -= time; // Apply downcount to all events. // This will result in a negative downcount for those events which are late. - for (TimingEvent* evt : s_events) + for (TimingEvent* event = s_active_events_head; event; event = event->next) { - evt->m_downcount -= time; - evt->m_time_since_last_run += time; + event->m_downcount -= time; + event->m_time_since_last_run += time; } // Now we can actually run the callbacks. - while (s_events.front()->m_downcount <= 0) + while (s_active_events_head->m_downcount <= 0) { - TimingEvent* evt = s_events.front(); - std::pop_heap(s_events.begin(), s_events.end(), CompareEvents); + // move it to the end, since that'll likely be its new position + TimingEvent* event = s_active_events_head; + s_current_event = event; // Factor late time into the time for the next invocation. - const TickCount ticks_late = -evt->m_downcount; - const TickCount ticks_to_execute = evt->m_time_since_last_run; - evt->m_downcount += evt->m_interval; - evt->m_time_since_last_run = 0; + const TickCount ticks_late = -event->m_downcount; + const TickCount ticks_to_execute = event->m_time_since_last_run; + event->m_downcount += event->m_interval; + event->m_time_since_last_run = 0; // The cycles_late is only an indicator, it doesn't modify the cycles to execute. - evt->m_callback(ticks_to_execute, ticks_late); - - // Place it in the appropriate position in the queue. - if (s_events_need_sorting) - { - // Another event may have been changed by this event, or the interval/downcount changed. - std::make_heap(s_events.begin(), s_events.end(), CompareEvents); - s_events_need_sorting = false; - } - else - { - // Keep the event list in a heap. The event we just serviced will be in the last place, - // so we can use push_here instead of make_heap, which should be faster. - std::push_heap(s_events.begin(), s_events.end(), CompareEvents); - } + event->m_callback(ticks_to_execute, ticks_late); + if (event->m_active) + SortEvent(event); } } s_last_event_run_time = s_global_tick_counter; - s_running_events = false; + s_current_event = nullptr; UpdateCPUDowncount(); } @@ -216,21 +339,21 @@ bool DoState(StateWrapper& sw) } else { - u32 event_count = static_cast(s_events.size()); - sw.Do(&event_count); - for (TimingEvent* evt : s_events) + sw.Do(&s_active_event_count); + + for (TimingEvent* event = s_active_events_head; event; event = event->next) { - sw.Do(&evt->m_name); - sw.Do(&evt->m_downcount); - sw.Do(&evt->m_time_since_last_run); - sw.Do(&evt->m_period); - sw.Do(&evt->m_interval); + sw.Do(&event->m_name); + sw.Do(&event->m_downcount); + sw.Do(&event->m_time_since_last_run); + sw.Do(&event->m_period); + sw.Do(&event->m_interval); } sw.Do(&s_last_event_run_time); - Log_DevPrintf("Wrote %u events to save state.", event_count); + Log_DevPrintf("Wrote %u events to save state.", s_active_event_count); } return !sw.HasError(); @@ -276,7 +399,8 @@ void TimingEvent::Schedule(TickCount ticks) { // Event is already active, so we leave the time since last run alone, and just modify the downcount. // If this is a call from an IO handler for example, re-sort the event queue. - TimingEvents::SortEvents(); + if (TimingEvents::s_current_event != this) + TimingEvents::SortEvent(this); } } @@ -300,7 +424,8 @@ void TimingEvent::Reset() m_downcount = m_interval; m_time_since_last_run = 0; - TimingEvents::SortEvents(); + if (TimingEvents::s_current_event != this) + TimingEvents::SortEvent(this); } void TimingEvent::InvokeEarly(bool force /* = false */) @@ -318,7 +443,8 @@ void TimingEvent::InvokeEarly(bool force /* = false */) m_callback(ticks_to_execute, 0); // Since we've changed the downcount, we need to re-sort the events. - TimingEvents::SortEvents(); + DebugAssert(TimingEvents::s_current_event != this); + TimingEvents::SortEvent(this); } void TimingEvent::Activate() diff --git a/src/core/timing_event.h b/src/core/timing_event.h index 0238b3402..ca58ddbdf 100644 --- a/src/core/timing_event.h +++ b/src/core/timing_event.h @@ -56,6 +56,9 @@ public: void SetInterval(TickCount interval) { m_interval = interval; } void SetPeriod(TickCount period) { m_period = period; } + TimingEvent* prev = nullptr; + TimingEvent* next = nullptr; + TickCount m_downcount; TickCount m_time_since_last_run; TickCount m_period; From d59eb05d948f4b16f9afb9cf65ace0fdc925bae6 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Mon, 31 Aug 2020 22:01:05 +1000 Subject: [PATCH 36/61] Timers: Fix update interval for timer2 IRQs being too low --- src/core/timers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/timers.cpp b/src/core/timers.cpp index fc7a06396..a8c480340 100644 --- a/src/core/timers.cpp +++ b/src/core/timers.cpp @@ -349,7 +349,7 @@ TickCount Timers::GetTicksUntilNextInterrupt() const min_ticks_for_this_timer = std::min(min_ticks_for_this_timer, static_cast(0xFFFF - cs.counter)); if (cs.external_counting_enabled) // sysclk/8 for timer 2 - min_ticks_for_this_timer = std::max(1, min_ticks_for_this_timer / 8); + min_ticks_for_this_timer = std::max(1, min_ticks_for_this_timer * 8); min_ticks = std::min(min_ticks, min_ticks_for_this_timer); } From e21fc9e253cf8b2bd0787eddd3d5decc818fe6b1 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 1 Sep 2020 12:02:04 +1000 Subject: [PATCH 37/61] GPU/Vulkan: Use half width framebuffer for VRAM readbacks --- src/core/gpu_hw_vulkan.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/gpu_hw_vulkan.cpp b/src/core/gpu_hw_vulkan.cpp index efc51ef25..808abde7c 100644 --- a/src/core/gpu_hw_vulkan.cpp +++ b/src/core/gpu_hw_vulkan.cpp @@ -394,7 +394,7 @@ bool GPU_HW_Vulkan::CreateFramebuffer() !m_vram_readback_texture.Create(VRAM_WIDTH, VRAM_HEIGHT, 1, 1, texture_format, samples, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT) || - !m_vram_readback_staging_texture.Create(Vulkan::StagingBuffer::Type::Readback, texture_format, VRAM_WIDTH, + !m_vram_readback_staging_texture.Create(Vulkan::StagingBuffer::Type::Readback, texture_format, VRAM_WIDTH / 2, VRAM_HEIGHT)) { return false; @@ -1019,7 +1019,8 @@ void GPU_HW_Vulkan::ReadVRAM(u32 x, u32 y, u32 width, u32 height) // Work around Mali driver bug: set full framebuffer size for render area. The GPU crashes with a page fault if we use // the actual size we're rendering to... - BeginRenderPass(m_vram_readback_render_pass, m_vram_readback_framebuffer, 0, 0, VRAM_WIDTH, VRAM_HEIGHT); + BeginRenderPass(m_vram_readback_render_pass, m_vram_readback_framebuffer, 0, 0, m_vram_readback_texture.GetWidth(), + m_vram_readback_texture.GetHeight()); // Encode the 24-bit texture as 16-bit. const u32 uniforms[4] = {copy_rect.left, copy_rect.top, copy_rect.GetWidth(), copy_rect.GetHeight()}; From dd0ae0fc9d7a39584d0af22d70840c3e3cb4d29b Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 1 Sep 2020 12:02:29 +1000 Subject: [PATCH 38/61] Vulkan/StagingTexture: Keep mapped throughout transfers The underlying bug here was not invalidating the buffer after mapping (is this supposed to be necessary?). But by keeping it mapped, we invalidate it anyway. Fixes screen corruption in Final Fantasy IX on Mali GPUs. --- src/common/vulkan/staging_texture.cpp | 26 ++++++++------------------ src/common/vulkan/staging_texture.h | 2 +- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/common/vulkan/staging_texture.cpp b/src/common/vulkan/staging_texture.cpp index 0403a1164..e9b62d598 100644 --- a/src/common/vulkan/staging_texture.cpp +++ b/src/common/vulkan/staging_texture.cpp @@ -209,10 +209,8 @@ void StagingTexture::Flush() void StagingTexture::ReadTexels(u32 src_x, u32 src_y, u32 width, u32 height, void* out_ptr, u32 out_stride) { Assert(m_staging_buffer.GetType() != StagingBuffer::Type::Upload); - if (!PrepareForAccess()) - return; - Assert((src_x + width) <= m_width && (src_y + height) <= m_height); + PrepareForAccess(); // Offset pointer to point to start of region being copied out. const char* current_ptr = m_staging_buffer.GetMapPointer(); @@ -239,10 +237,9 @@ void StagingTexture::ReadTexels(u32 src_x, u32 src_y, u32 width, u32 height, voi void StagingTexture::ReadTexel(u32 x, u32 y, void* out_ptr) { Assert(m_staging_buffer.GetType() != StagingBuffer::Type::Upload); - if (!PrepareForAccess()) - return; - Assert(x < m_width && y < m_height); + PrepareForAccess(); + const char* src_ptr = GetMappedPointer() + y * GetMappedStride() + x * m_texel_size; std::memcpy(out_ptr, src_ptr, m_texel_size); } @@ -250,10 +247,8 @@ void StagingTexture::ReadTexel(u32 x, u32 y, void* out_ptr) void StagingTexture::WriteTexels(u32 dst_x, u32 dst_y, u32 width, u32 height, const void* in_ptr, u32 in_stride) { Assert(m_staging_buffer.GetType() != StagingBuffer::Type::Readback); - if (!PrepareForAccess()) - return; - Assert((dst_x + width) <= m_width && (dst_y + height) <= m_height); + PrepareForAccess(); // Offset pointer to point to start of region being copied to. char* current_ptr = GetMappedPointer(); @@ -279,23 +274,18 @@ void StagingTexture::WriteTexels(u32 dst_x, u32 dst_y, u32 width, u32 height, co void StagingTexture::WriteTexel(u32 x, u32 y, const void* in_ptr) { - if (!PrepareForAccess()) - return; - Assert(x < m_width && y < m_height); + PrepareForAccess(); + char* dest_ptr = GetMappedPointer() + y * m_map_stride + x * m_texel_size; std::memcpy(dest_ptr, in_ptr, m_texel_size); } -bool StagingTexture::PrepareForAccess() +void StagingTexture::PrepareForAccess() { + Assert(IsMapped()); if (m_needs_flush) - { - if (IsMapped()) - Unmap(); Flush(); - } - return IsMapped() || Map(); } } // namespace Vulkan \ No newline at end of file diff --git a/src/common/vulkan/staging_texture.h b/src/common/vulkan/staging_texture.h index 7f03e04dd..f8d24836f 100644 --- a/src/common/vulkan/staging_texture.h +++ b/src/common/vulkan/staging_texture.h @@ -73,7 +73,7 @@ public: void WriteTexel(u32 x, u32 y, const void* in_ptr); private: - bool PrepareForAccess(); + void PrepareForAccess(); StagingBuffer m_staging_buffer; u64 m_flush_fence_counter = 0; From c5044768a8527f4fcf32b4cda241143a4ea9b98b Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 1 Sep 2020 12:03:51 +1000 Subject: [PATCH 39/61] Android: Hook up game list long press menu --- .../stenzek/duckstation/MainActivity.java | 8 ++++++++ .../main/res/menu/menu_game_list_entry.xml | 20 ++++--------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java index be2063b55..a2f9523e3 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java @@ -85,6 +85,14 @@ public class MainActivity extends AppCompatActivity { menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { + int id = item.getItemId(); + if (id == R.id.game_list_entry_menu_start_game) { + startEmulation(mGameList.getEntry(position).getPath(), false); + return true; + } else if (id == R.id.game_list_entry_menu_resume_game) { + startEmulation(mGameList.getEntry(position).getPath(), true); + return true; + } return false; } }); diff --git a/android/app/src/main/res/menu/menu_game_list_entry.xml b/android/app/src/main/res/menu/menu_game_list_entry.xml index 1fb01d997..37ab82ceb 100644 --- a/android/app/src/main/res/menu/menu_game_list_entry.xml +++ b/android/app/src/main/res/menu/menu_game_list_entry.xml @@ -1,22 +1,10 @@ - + android:id="@+id/game_list_entry_menu_start_game" + android:title="Start Game" /> - - - - - - - + android:id="@+id/game_list_entry_menu_resume_game" + android:title="Resume Game" /> \ No newline at end of file From 41558f4df31d13e658e0191b994d075a306bc0fd Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 1 Sep 2020 12:03:59 +1000 Subject: [PATCH 40/61] Android: Sort game list by title --- .../java/com/github/stenzek/duckstation/GameList.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java index 75d15487b..ec23b9c56 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java @@ -12,6 +12,8 @@ import android.widget.TextView; import androidx.preference.PreferenceManager; +import java.util.Arrays; +import java.util.Comparator; import java.util.Set; public class GameList { @@ -25,10 +27,19 @@ public class GameList { mEntries = new GameListEntry[0]; } + private class GameListEntryComparator implements Comparator { + @Override + public int compare(GameListEntry left, GameListEntry right) { + return left.getTitle().compareTo(right.getTitle()); + } + } + + public void refresh(boolean invalidateCache, boolean invalidateDatabase) { // Search and get entries from native code AndroidHostInterface.getInstance().refreshGameList(invalidateCache, invalidateDatabase); mEntries = AndroidHostInterface.getInstance().getGameListEntries(); + Arrays.sort(mEntries, new GameListEntryComparator()); mAdapter.notifyDataSetChanged(); } From 13e30958011a67cb6c1d56a9f585b4f5f3ad8cc7 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 1 Sep 2020 12:13:56 +1000 Subject: [PATCH 41/61] Core: Don't link to imgui for libretro core --- dep/CMakeLists.txt | 6 ++++-- src/core/CMakeLists.txt | 7 ++++++- src/core/cdrom.cpp | 6 +++++- src/core/core.vcxproj | 16 ++++++++-------- src/core/gpu.cpp | 6 +++++- src/core/gpu_hw.cpp | 6 +++++- src/core/host_interface.cpp | 1 - src/core/mdec.cpp | 6 +++++- src/core/spu.cpp | 6 +++++- src/core/system.cpp | 1 - src/core/timers.cpp | 6 +++++- 11 files changed, 48 insertions(+), 19 deletions(-) diff --git a/dep/CMakeLists.txt b/dep/CMakeLists.txt index 13581d697..79acdde2b 100644 --- a/dep/CMakeLists.txt +++ b/dep/CMakeLists.txt @@ -1,7 +1,6 @@ add_subdirectory(cubeb) add_subdirectory(glad) add_subdirectory(googletest) -add_subdirectory(imgui) add_subdirectory(libcue) add_subdirectory(simpleini) add_subdirectory(stb) @@ -13,10 +12,13 @@ add_subdirectory(libFLAC) add_subdirectory(libchdr) add_subdirectory(xxhash) add_subdirectory(rapidjson) - add_subdirectory(glslang) add_subdirectory(vulkan-loader) +if(NOT BUILD_LIBRETRO_CORE) + add_subdirectory(imgui) +endif() + if(ENABLE_DISCORD_PRESENCE) add_subdirectory(discord-rpc) endif() diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 4d2cbad5f..0517bc810 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -98,7 +98,7 @@ set(RECOMPILER_SRCS target_include_directories(core PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") -target_link_libraries(core PUBLIC Threads::Threads common imgui tinyxml2 zlib vulkan-loader simpleini) +target_link_libraries(core PUBLIC Threads::Threads common tinyxml2 zlib vulkan-loader simpleini) target_link_libraries(core PRIVATE glad stb) if(WIN32) @@ -126,3 +126,8 @@ elseif(${CPU_ARCH} STREQUAL "aarch64") else() message("Not building recompiler") endif() + +if(NOT BUILD_LIBRETRO_CORE) + target_link_libraries(core PRIVATE imgui) + target_compile_definitions(core PRIVATE "WITH_IMGUI=1") +endif() diff --git a/src/core/cdrom.cpp b/src/core/cdrom.cpp index 0c8c17716..673e186d8 100644 --- a/src/core/cdrom.cpp +++ b/src/core/cdrom.cpp @@ -4,11 +4,13 @@ #include "common/state_wrapper.h" #include "dma.h" #include "game_list.h" -#include "imgui.h" #include "interrupt_controller.h" #include "settings.h" #include "spu.h" #include "system.h" +#ifdef WITH_IMGUI +#include "imgui.h" +#endif Log_SetChannel(CDROM); struct CommandInfo @@ -2341,6 +2343,7 @@ void CDROM::ClearSectorBuffers() void CDROM::DrawDebugWindow() { +#ifdef WITH_IMGUI static const ImVec4 active_color{1.0f, 1.0f, 1.0f, 1.0f}; static const ImVec4 inactive_color{0.4f, 0.4f, 0.4f, 1.0f}; const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x; @@ -2521,4 +2524,5 @@ void CDROM::DrawDebugWindow() } ImGui::End(); +#endif } diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index bc5aeadf1..85d116da7 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -304,7 +304,7 @@ Level4 Disabled - WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -330,7 +330,7 @@ Level4 Disabled - WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -356,7 +356,7 @@ Level4 Disabled - WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -385,7 +385,7 @@ Level4 Disabled - WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -415,7 +415,7 @@ MaxSpeed true - WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false @@ -442,7 +442,7 @@ MaxSpeed true - WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true @@ -470,7 +470,7 @@ MaxSpeed true - WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false @@ -497,7 +497,7 @@ MaxSpeed true - WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true diff --git a/src/core/gpu.cpp b/src/core/gpu.cpp index c56657c45..32d36ddcf 100644 --- a/src/core/gpu.cpp +++ b/src/core/gpu.cpp @@ -10,7 +10,9 @@ #include "system.h" #include "timers.h" #include -#include +#ifdef WITH_IMGUI +#include "imgui.h" +#endif Log_SetChannel(GPU); std::unique_ptr g_gpu; @@ -1341,6 +1343,7 @@ bool GPU::DumpVRAMToFile(const char* filename, u32 width, u32 height, u32 stride void GPU::DrawDebugStateWindow() { +#ifdef WITH_IMGUI const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x; ImGui::SetNextWindowSize(ImVec2(450.0f * framebuffer_scale, 550.0f * framebuffer_scale), ImGuiCond_FirstUseEver); @@ -1451,6 +1454,7 @@ void GPU::DrawDebugStateWindow() } ImGui::End(); +#endif } void GPU::DrawRendererStats(bool is_idle_frame) {} diff --git a/src/core/gpu_hw.cpp b/src/core/gpu_hw.cpp index 7dfe6f432..4741a9263 100644 --- a/src/core/gpu_hw.cpp +++ b/src/core/gpu_hw.cpp @@ -3,13 +3,15 @@ #include "common/log.h" #include "common/state_wrapper.h" #include "cpu_core.h" -#include "imgui.h" #include "pgxp.h" #include "settings.h" #include "system.h" #include #include #include +#ifdef WITH_IMGUI +#include "imgui.h" +#endif Log_SetChannel(GPU_HW); template @@ -1000,6 +1002,7 @@ void GPU_HW::DrawRendererStats(bool is_idle_frame) m_renderer_stats = {}; } +#ifdef WITH_IMGUI if (ImGui::CollapsingHeader("Renderer Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { static const ImVec4 active_color{1.0f, 1.0f, 1.0f, 1.0f}; @@ -1068,4 +1071,5 @@ void GPU_HW::DrawRendererStats(bool is_idle_frame) ImGui::Columns(1); } +#endif } diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index 9322a5f62..340c87715 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include Log_SetChannel(HostInterface); diff --git a/src/core/mdec.cpp b/src/core/mdec.cpp index 1d83e3435..cb1d752eb 100644 --- a/src/core/mdec.cpp +++ b/src/core/mdec.cpp @@ -5,7 +5,9 @@ #include "dma.h" #include "interrupt_controller.h" #include "system.h" -#include +#ifdef WITH_IMGUI +#include "imgui.h" +#endif Log_SetChannel(MDEC); MDEC g_mdec; @@ -701,6 +703,7 @@ void MDEC::HandleSetScaleCommand() void MDEC::DrawDebugStateWindow() { +#ifdef WITH_IMGUI const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x; ImGui::SetNextWindowSize(ImVec2(300.0f * framebuffer_scale, 350.0f * framebuffer_scale), ImGuiCond_FirstUseEver); @@ -738,4 +741,5 @@ void MDEC::DrawDebugStateWindow() } ImGui::End(); +#endif } diff --git a/src/core/spu.cpp b/src/core/spu.cpp index e0d42711f..ecb8b57a6 100644 --- a/src/core/spu.cpp +++ b/src/core/spu.cpp @@ -8,7 +8,9 @@ #include "host_interface.h" #include "interrupt_controller.h" #include "system.h" -#include +#ifdef WITH_IMGUI +#include "imgui.h" +#endif Log_SetChannel(SPU); SPU g_spu; @@ -1747,6 +1749,7 @@ void SPU::ProcessReverb(s16 left_in, s16 right_in, s32* left_out, s32* right_out void SPU::DrawDebugStateWindow() { +#ifdef WITH_IMGUI static const ImVec4 active_color{1.0f, 1.0f, 1.0f, 1.0f}; static const ImVec4 inactive_color{0.4f, 0.4f, 0.4f, 1.0f}; const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x; @@ -1924,4 +1927,5 @@ void SPU::DrawDebugStateWindow() } ImGui::End(); +#endif } diff --git a/src/core/system.cpp b/src/core/system.cpp index 50ab3d24d..3f24ea4ea 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -27,7 +27,6 @@ #include "spu.h" #include "timers.h" #include -#include #include Log_SetChannel(System); diff --git a/src/core/timers.cpp b/src/core/timers.cpp index a8c480340..a51b8bd7c 100644 --- a/src/core/timers.cpp +++ b/src/core/timers.cpp @@ -4,7 +4,9 @@ #include "gpu.h" #include "interrupt_controller.h" #include "system.h" -#include +#ifdef WITH_IMGUI +#include "imgui.h" +#endif Log_SetChannel(Timers); Timers g_timers; @@ -369,6 +371,7 @@ void Timers::UpdateSysClkEvent() void Timers::DrawDebugStateWindow() { +#ifdef WITH_IMGUI static constexpr u32 NUM_COLUMNS = 10; static constexpr std::array column_names = { {"#", "Value", "Target", "Sync", "Reset", "IRQ", "IRQRepeat", "IRQToggle", "Clock Source", "Reached"}}; @@ -437,4 +440,5 @@ void Timers::DrawDebugStateWindow() ImGui::Columns(1); ImGui::End(); +#endif } From 6bbbb96d4bbfeaca33a43e1597973b55015b43f5 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 1 Sep 2020 12:29:22 +1000 Subject: [PATCH 42/61] Move GameList to FrontendCommon Reduces libretro core dependencies further. --- .../app/src/cpp/android_host_interface.cpp | 2 +- dep/CMakeLists.txt | 4 +- src/core/CMakeLists.txt | 6 +- src/core/cdrom.cpp | 3 +- src/core/core.vcxproj | 30 +- src/core/core.vcxproj.filters | 4 - src/core/system.cpp | 264 ++++++++++++++++- src/core/system.h | 20 ++ .../libretro_host_interface.cpp | 5 +- src/duckstation-qt/gamelistmodel.cpp | 9 +- src/duckstation-qt/gamelistmodel.h | 2 +- src/duckstation-qt/gamelistsettingswidget.cpp | 2 +- src/duckstation-qt/gamelistwidget.cpp | 2 +- src/duckstation-qt/gamepropertiesdialog.cpp | 2 +- src/duckstation-qt/gamepropertiesdialog.h | 2 +- src/duckstation-qt/mainwindow.cpp | 2 +- src/duckstation-qt/qthostinterface.cpp | 2 +- src/frontend-common/CMakeLists.txt | 6 +- src/frontend-common/common_host_interface.cpp | 8 +- src/frontend-common/frontend-common.vcxproj | 23 +- .../frontend-common.vcxproj.filters | 4 + src/{core => frontend-common}/game_list.cpp | 279 +----------------- src/{core => frontend-common}/game_list.h | 22 +- .../game_settings.cpp | 4 +- src/{core => frontend-common}/game_settings.h | 2 +- 25 files changed, 356 insertions(+), 353 deletions(-) rename src/{core => frontend-common}/game_list.cpp (81%) rename src/{core => frontend-common}/game_list.h (85%) rename src/{core => frontend-common}/game_settings.cpp (99%) rename src/{core => frontend-common}/game_settings.h (97%) diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index 682411a85..8c5e18936 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -5,10 +5,10 @@ #include "common/string.h" #include "common/timestamp.h" #include "core/controller.h" -#include "core/game_list.h" #include "core/gpu.h" #include "core/host_display.h" #include "core/system.h" +#include "frontend-common/game_list.h" #include "frontend-common/imgui_styles.h" #include "frontend-common/opengl_host_display.h" #include "frontend-common/vulkan_host_display.h" diff --git a/dep/CMakeLists.txt b/dep/CMakeLists.txt index 79acdde2b..5b00dbbfa 100644 --- a/dep/CMakeLists.txt +++ b/dep/CMakeLists.txt @@ -2,9 +2,7 @@ add_subdirectory(cubeb) add_subdirectory(glad) add_subdirectory(googletest) add_subdirectory(libcue) -add_subdirectory(simpleini) add_subdirectory(stb) -add_subdirectory(tinyxml2) add_subdirectory(zlib) add_subdirectory(minizip) add_subdirectory(lzma) @@ -17,6 +15,8 @@ add_subdirectory(vulkan-loader) if(NOT BUILD_LIBRETRO_CORE) add_subdirectory(imgui) + add_subdirectory(simpleini) + add_subdirectory(tinyxml2) endif() if(ENABLE_DISCORD_PRESENCE) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 0517bc810..c8e2f074a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -24,10 +24,6 @@ add_library(core digital_controller.h dma.cpp dma.h - game_list.cpp - game_list.h - game_settings.cpp - game_settings.h gpu.cpp gpu.h gpu_commands.cpp @@ -98,7 +94,7 @@ set(RECOMPILER_SRCS target_include_directories(core PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") -target_link_libraries(core PUBLIC Threads::Threads common tinyxml2 zlib vulkan-loader simpleini) +target_link_libraries(core PUBLIC Threads::Threads common zlib vulkan-loader) target_link_libraries(core PRIVATE glad stb) if(WIN32) diff --git a/src/core/cdrom.cpp b/src/core/cdrom.cpp index 673e186d8..033d5d9b5 100644 --- a/src/core/cdrom.cpp +++ b/src/core/cdrom.cpp @@ -3,7 +3,6 @@ #include "common/log.h" #include "common/state_wrapper.h" #include "dma.h" -#include "game_list.h" #include "interrupt_controller.h" #include "settings.h" #include "spu.h" @@ -446,7 +445,7 @@ void CDROM::InsertMedia(std::unique_ptr media) RemoveMedia(); // set the region from the system area of the disc - m_disc_region = GameList::GetRegionForImage(media.get()); + m_disc_region = System::GetRegionForImage(media.get()); Log_InfoPrintf("Inserting new media, disc region: %s, console region: %s", Settings::GetDiscRegionName(m_disc_region), Settings::GetConsoleRegionName(System::GetRegion())); diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 85d116da7..cd02f72f2 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -59,8 +59,6 @@ - - @@ -107,8 +105,6 @@ - - @@ -147,14 +143,14 @@ {bb08260f-6fbc-46af-8924-090ee71360c6} - - {3773f4cc-614e-4028-8595-22e08ca649e3} - {ed601289-ac1a-46b8-a8ed-17db9eb73423} - - {933118a9-68c5-47b4-b151-b03c93961623} + + {9c8ddeb0-2b8f-4f5f-ba86-127cdf27f035} + + + {7ff9fdb9-d504-47db-a16a-b08071999620} {ee054e08-3799-4a59-a422-18259c105ffd} @@ -307,7 +303,7 @@ WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false stdcpp17 @@ -333,7 +329,7 @@ WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false stdcpp17 @@ -359,7 +355,7 @@ WITH_IMGUI=1;WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default true false @@ -388,7 +384,7 @@ WITH_IMGUI=1;WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default true false @@ -416,7 +412,7 @@ MaxSpeed true WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false stdcpp17 @@ -443,7 +439,7 @@ MaxSpeed true WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 @@ -471,7 +467,7 @@ MaxSpeed true WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false stdcpp17 @@ -498,7 +494,7 @@ MaxSpeed true WITH_IMGUI=1;WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 0d86f8170..96b2c050b 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -31,7 +31,6 @@ - @@ -47,7 +46,6 @@ - @@ -82,7 +80,6 @@ - @@ -98,6 +95,5 @@ - \ No newline at end of file diff --git a/src/core/system.cpp b/src/core/system.cpp index 3f24ea4ea..3d4630663 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -4,6 +4,7 @@ #include "cdrom.h" #include "common/audio_stream.h" #include "common/file_system.h" +#include "common/iso_reader.h" #include "common/log.h" #include "common/state_wrapper.h" #include "common/string_util.h" @@ -11,7 +12,6 @@ #include "cpu_code_cache.h" #include "cpu_core.h" #include "dma.h" -#include "game_list.h" #include "gpu.h" #include "gte.h" #include "host_display.h" @@ -27,6 +27,7 @@ #include "spu.h" #include "timers.h" #include +#include #include Log_SetChannel(System); @@ -203,6 +204,71 @@ float GetThrottleFrequency() return s_throttle_frequency; } +bool IsExeFileName(const char* path) +{ + const char* extension = std::strrchr(path, '.'); + return (extension && + (StringUtil::Strcasecmp(extension, ".exe") == 0 || StringUtil::Strcasecmp(extension, ".psexe") == 0)); +} + +bool IsPsfFileName(const char* path) +{ + const char* extension = std::strrchr(path, '.'); + return (extension && StringUtil::Strcasecmp(extension, ".psf") == 0); +} + +bool IsM3UFileName(const char* path) +{ + const char* extension = std::strrchr(path, '.'); + return (extension && StringUtil::Strcasecmp(extension, ".m3u") == 0); +} + +std::vector ParseM3UFile(const char* path) +{ + std::ifstream ifs(path); + if (!ifs.is_open()) + { + Log_ErrorPrintf("Failed to open %s", path); + return {}; + } + + std::vector entries; + std::string line; + while (std::getline(ifs, line)) + { + u32 start_offset = 0; + while (start_offset < line.size() && std::isspace(line[start_offset])) + start_offset++; + + // skip comments + if (start_offset == line.size() || line[start_offset] == '#') + continue; + + // strip ending whitespace + u32 end_offset = static_cast(line.size()) - 1; + while (std::isspace(line[end_offset]) && end_offset > start_offset) + end_offset--; + + // anything? + if (start_offset == end_offset) + continue; + + std::string entry_path(line.begin() + start_offset, line.begin() + end_offset + 1); + if (!FileSystem::IsAbsolutePath(entry_path)) + { + SmallString absolute_path; + FileSystem::BuildPathRelativeToFile(absolute_path, path, entry_path.c_str()); + entry_path = absolute_path; + } + + Log_DevPrintf("Read path from m3u: '%s'", entry_path.c_str()); + entries.push_back(std::move(entry_path)); + } + + Log_InfoPrintf("Loaded %zu paths from m3u '%s'", entries.size(), path); + return entries; +} + ConsoleRegion GetConsoleRegionForDiscRegion(DiscRegion region) { switch (region) @@ -220,6 +286,188 @@ ConsoleRegion GetConsoleRegionForDiscRegion(DiscRegion region) } } +std::string_view GetTitleForPath(const char* path) +{ + const char* extension = std::strrchr(path, '.'); + if (path == extension) + return path; + + const char* path_end = path + std::strlen(path); + const char* title_end = extension ? (extension - 1) : (path_end); + const char* title_start = std::max(std::strrchr(path, '/'), std::strrchr(path, '\\')); + if (!title_start || title_start == path) + return std::string_view(path, title_end - title_start); + else + return std::string_view(title_start + 1, title_end - title_start); +} + +std::string GetGameCodeForPath(const char* image_path) +{ + std::unique_ptr cdi = CDImage::Open(image_path); + if (!cdi) + return {}; + + return GetGameCodeForImage(cdi.get()); +} + +std::string GetGameCodeForImage(CDImage* cdi) +{ + ISOReader iso; + if (!iso.Open(cdi, 1)) + return {}; + + // Read SYSTEM.CNF + std::vector system_cnf_data; + if (!iso.ReadFile("SYSTEM.CNF", &system_cnf_data)) + return {}; + + // Parse lines + std::vector> lines; + std::pair current_line; + bool reading_value = false; + for (size_t pos = 0; pos < system_cnf_data.size(); pos++) + { + const char ch = static_cast(system_cnf_data[pos]); + if (ch == '\r' || ch == '\n') + { + if (!current_line.first.empty()) + { + lines.push_back(std::move(current_line)); + current_line = {}; + reading_value = false; + } + } + else if (ch == ' ' || (ch >= 0x09 && ch <= 0x0D)) + { + continue; + } + else if (ch == '=' && !reading_value) + { + reading_value = true; + } + else + { + if (reading_value) + current_line.second.push_back(ch); + else + current_line.first.push_back(ch); + } + } + + if (!current_line.first.empty()) + lines.push_back(std::move(current_line)); + + // Find the BOOT line + auto iter = std::find_if(lines.begin(), lines.end(), + [](const auto& it) { return StringUtil::Strcasecmp(it.first.c_str(), "boot") == 0; }); + if (iter == lines.end()) + return {}; + + // cdrom:\SCES_123.45;1 + std::string code = iter->second; + std::string::size_type pos = code.rfind('\\'); + if (pos != std::string::npos) + { + code.erase(0, pos + 1); + } + else + { + // cdrom:SCES_123.45;1 + pos = code.rfind(':'); + if (pos != std::string::npos) + code.erase(0, pos + 1); + } + + pos = code.find(';'); + if (pos != std::string::npos) + code.erase(pos); + + // SCES_123.45 -> SCES-12345 + for (pos = 0; pos < code.size();) + { + if (code[pos] == '.') + { + code.erase(pos, 1); + continue; + } + + if (code[pos] == '_') + code[pos] = '-'; + else + code[pos] = static_cast(std::toupper(code[pos])); + + pos++; + } + + return code; +} + +DiscRegion GetRegionForCode(std::string_view code) +{ + std::string prefix; + for (size_t pos = 0; pos < code.length(); pos++) + { + const int ch = std::tolower(code[pos]); + if (ch < 'a' || ch > 'z') + break; + + prefix.push_back(static_cast(ch)); + } + + if (prefix == "sces" || prefix == "sced" || prefix == "sles" || prefix == "sled") + return DiscRegion::PAL; + else if (prefix == "scps" || prefix == "slps" || prefix == "slpm" || prefix == "sczs" || prefix == "papx") + return DiscRegion::NTSC_J; + else if (prefix == "scus" || prefix == "slus") + return DiscRegion::NTSC_U; + else + return DiscRegion::Other; +} + +DiscRegion GetRegionFromSystemArea(CDImage* cdi) +{ + // The license code is on sector 4 of the disc. + u8 sector[CDImage::DATA_SECTOR_SIZE]; + if (!cdi->Seek(1, 4) || cdi->Read(CDImage::ReadMode::DataOnly, 1, sector) != 1) + return DiscRegion::Other; + + static constexpr char ntsc_u_string[] = " Licensed by Sony Computer Entertainment Amer ica "; + static constexpr char ntsc_j_string[] = " Licensed by Sony Computer Entertainment Inc."; + static constexpr char pal_string[] = " Licensed by Sony Computer Entertainment Euro pe"; + + // subtract one for the terminating null + if (std::equal(ntsc_u_string, ntsc_u_string + countof(ntsc_u_string) - 1, sector)) + return DiscRegion::NTSC_U; + else if (std::equal(ntsc_j_string, ntsc_j_string + countof(ntsc_j_string) - 1, sector)) + return DiscRegion::NTSC_J; + else if (std::equal(pal_string, pal_string + countof(pal_string) - 1, sector)) + return DiscRegion::PAL; + else + return DiscRegion::Other; +} + +DiscRegion GetRegionForImage(CDImage* cdi) +{ + DiscRegion system_area_region = GetRegionFromSystemArea(cdi); + if (system_area_region != DiscRegion::Other) + return system_area_region; + + std::string code = GetGameCodeForImage(cdi); + if (code.empty()) + return DiscRegion::Other; + + return GetRegionForCode(code); +} + +std::optional GetRegionForPath(const char* image_path) +{ + std::unique_ptr cdi = CDImage::Open(image_path); + if (!cdi) + return {}; + + return GetRegionForImage(cdi.get()); +} + bool RecreateGPU(GPURenderer renderer) { g_gpu->RestoreGraphicsAPIState(); @@ -297,8 +545,8 @@ bool Boot(const SystemBootParameters& params) bool psf_boot = false; if (!params.filename.empty()) { - exe_boot = GameList::IsExeFileName(params.filename.c_str()); - psf_boot = (!exe_boot && GameList::IsPsfFileName(params.filename.c_str())); + exe_boot = IsExeFileName(params.filename.c_str()); + psf_boot = (!exe_boot && IsPsfFileName(params.filename.c_str())); if (exe_boot || psf_boot) { // TODO: Pull region from PSF @@ -311,9 +559,9 @@ bool Boot(const SystemBootParameters& params) else { u32 playlist_index; - if (GameList::IsM3UFileName(params.filename.c_str())) + if (IsM3UFileName(params.filename.c_str())) { - s_media_playlist = GameList::ParseM3UFile(params.filename.c_str()); + s_media_playlist = ParseM3UFile(params.filename.c_str()); s_media_playlist_filename = params.filename; if (s_media_playlist.empty()) { @@ -350,7 +598,7 @@ bool Boot(const SystemBootParameters& params) if (s_region == ConsoleRegion::Auto) { - const DiscRegion disc_region = GameList::GetRegionForImage(media.get()); + const DiscRegion disc_region = GetRegionForImage(media.get()); if (disc_region != DiscRegion::Other) { s_region = GetConsoleRegionForDiscRegion(disc_region); @@ -689,7 +937,7 @@ bool DoLoadState(ByteStream* state, bool force_software_renderer) return false; } - playlist_entries = GameList::ParseM3UFile(playlist_filename.c_str()); + playlist_entries = ParseM3UFile(playlist_filename.c_str()); if (playlist_entries.empty()) { g_host_interface->ReportFormattedError("Failed to load save state playlist entries from '%s'", @@ -1191,7 +1439,7 @@ void UpdateMemoryCards() { if (!s_media_playlist_filename.empty() && g_settings.memory_card_use_playlist_title) { - const std::string playlist_title(GameList::GetTitleForPath(s_media_playlist_filename.c_str())); + const std::string playlist_title(GetTitleForPath(s_media_playlist_filename.c_str())); card = MemoryCard::Open(g_host_interface->GetGameMemoryCardPath(playlist_title.c_str(), i)); } else if (s_running_game_title.empty()) diff --git a/src/core/system.h b/src/core/system.h index eee1f6973..a6fde2ac5 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -45,9 +45,29 @@ enum class State Paused }; +/// Returns true if the filename is a PlayStation executable we can inject. +bool IsExeFileName(const char* path); + +/// Returns true if the filename is a Portable Sound Format file we can uncompress/load. +bool IsPsfFileName(const char* path); + +/// Returns true if the filename is a M3U Playlist we can handle. +bool IsM3UFileName(const char* path); + +/// Parses an M3U playlist, returning the entries. +std::vector ParseM3UFile(const char* path); + /// Returns the preferred console type for a disc. ConsoleRegion GetConsoleRegionForDiscRegion(DiscRegion region); +std::string GetGameCodeForImage(CDImage* cdi); +std::string GetGameCodeForPath(const char* image_path); +DiscRegion GetRegionForCode(std::string_view code); +DiscRegion GetRegionFromSystemArea(CDImage* cdi); +DiscRegion GetRegionForImage(CDImage* cdi); +std::optional GetRegionForPath(const char* image_path); +std::string_view GetTitleForPath(const char* path); + State GetState(); void SetState(State new_state); bool IsRunning(); diff --git a/src/duckstation-libretro/libretro_host_interface.cpp b/src/duckstation-libretro/libretro_host_interface.cpp index 8388ae868..09feaa9c5 100644 --- a/src/duckstation-libretro/libretro_host_interface.cpp +++ b/src/duckstation-libretro/libretro_host_interface.cpp @@ -7,7 +7,6 @@ #include "core/analog_controller.h" #include "core/bus.h" #include "core/digital_controller.h" -#include "core/game_list.h" #include "core/gpu.h" #include "core/system.h" #include "libretro_audio_stream.h" @@ -132,7 +131,7 @@ bool LibretroHostInterface::ConfirmMessage(const char* message) void LibretroHostInterface::GetGameInfo(const char* path, CDImage* image, std::string* code, std::string* title) { // Just use the filename for now... we don't have the game list. Unless we can pull this from the frontend somehow? - *title = GameList::GetTitleForPath(path); + *title = System::GetTitleForPath(path); code->clear(); } @@ -1158,7 +1157,7 @@ bool LibretroHostInterface::DiskControlGetImageLabel(unsigned index, char* label if (image_path.empty()) return false; - const std::string_view title = GameList::GetTitleForPath(label); + const std::string_view title = System::GetTitleForPath(label); StringUtil::Strlcpy(label, title, len); Log_DevPrintf("DiskControlGetImagePath(%u) -> %s", index, label); return true; diff --git a/src/duckstation-qt/gamelistmodel.cpp b/src/duckstation-qt/gamelistmodel.cpp index c3e422bb8..7946dbeba 100644 --- a/src/duckstation-qt/gamelistmodel.cpp +++ b/src/duckstation-qt/gamelistmodel.cpp @@ -1,5 +1,6 @@ #include "gamelistmodel.h" #include "common/string_util.h" +#include "core/system.h" static constexpr std::array s_column_names = { {"Type", "Code", "Title", "File Title", "Size", "Region", "Compatibility"}}; @@ -69,7 +70,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const case Column_FileTitle: { - const std::string_view file_title(GameList::GetTitleForPath(ge.path.c_str())); + const std::string_view file_title(System::GetTitleForPath(ge.path.c_str())); return QString::fromUtf8(file_title.data(), static_cast(file_title.length())); } @@ -96,7 +97,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const case Column_FileTitle: { - const std::string_view file_title(GameList::GetTitleForPath(ge.path.c_str())); + const std::string_view file_title(System::GetTitleForPath(ge.path.c_str())); return QString::fromUtf8(file_title.data(), static_cast(file_title.length())); } @@ -234,8 +235,8 @@ bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& r case Column_FileTitle: { - const std::string_view file_title_left(GameList::GetTitleForPath(left.path.c_str())); - const std::string_view file_title_right(GameList::GetTitleForPath(right.path.c_str())); + const std::string_view file_title_left(System::GetTitleForPath(left.path.c_str())); + const std::string_view file_title_right(System::GetTitleForPath(right.path.c_str())); if (file_title_left == file_title_right) return titlesLessThan(left_row, right_row, ascending); diff --git a/src/duckstation-qt/gamelistmodel.h b/src/duckstation-qt/gamelistmodel.h index 547555951..edf04892e 100644 --- a/src/duckstation-qt/gamelistmodel.h +++ b/src/duckstation-qt/gamelistmodel.h @@ -1,6 +1,6 @@ #pragma once -#include "core/game_list.h" #include "core/types.h" +#include "frontend-common/game_list.h" #include #include #include diff --git a/src/duckstation-qt/gamelistsettingswidget.cpp b/src/duckstation-qt/gamelistsettingswidget.cpp index c298d3527..268e2490f 100644 --- a/src/duckstation-qt/gamelistsettingswidget.cpp +++ b/src/duckstation-qt/gamelistsettingswidget.cpp @@ -2,7 +2,7 @@ #include "common/assert.h" #include "common/minizip_helpers.h" #include "common/string_util.h" -#include "core/game_list.h" +#include "frontend-common/game_list.h" #include "gamelistsearchdirectoriesmodel.h" #include "qthostinterface.h" #include "qtutils.h" diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp index e06aa3f7f..2f4325dcd 100644 --- a/src/duckstation-qt/gamelistwidget.cpp +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -1,7 +1,7 @@ #include "gamelistwidget.h" #include "common/string_util.h" -#include "core/game_list.h" #include "core/settings.h" +#include "frontend-common/game_list.h" #include "gamelistmodel.h" #include "qthostinterface.h" #include "qtutils.h" diff --git a/src/duckstation-qt/gamepropertiesdialog.cpp b/src/duckstation-qt/gamepropertiesdialog.cpp index 1cae9feb7..1564b54ca 100644 --- a/src/duckstation-qt/gamepropertiesdialog.cpp +++ b/src/duckstation-qt/gamepropertiesdialog.cpp @@ -1,8 +1,8 @@ #include "gamepropertiesdialog.h" #include "common/cd_image.h" #include "common/cd_image_hasher.h" -#include "core/game_list.h" #include "core/settings.h" +#include "frontend-common/game_list.h" #include "qthostinterface.h" #include "qtprogresscallback.h" #include "qtutils.h" diff --git a/src/duckstation-qt/gamepropertiesdialog.h b/src/duckstation-qt/gamepropertiesdialog.h index 74c0def22..65f74c22e 100644 --- a/src/duckstation-qt/gamepropertiesdialog.h +++ b/src/duckstation-qt/gamepropertiesdialog.h @@ -1,5 +1,5 @@ #pragma once -#include "core/game_settings.h" +#include "frontend-common/game_settings.h" #include "ui_gamepropertiesdialog.h" #include #include diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index c600ec749..756951b0f 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -2,10 +2,10 @@ #include "aboutdialog.h" #include "autoupdaterdialog.h" #include "common/assert.h" -#include "core/game_list.h" #include "core/host_display.h" #include "core/settings.h" #include "core/system.h" +#include "frontend-common/game_list.h" #include "gamelistsettingswidget.h" #include "gamelistwidget.h" #include "gamepropertiesdialog.h" diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp index a1b60a00b..ddacf57e3 100644 --- a/src/duckstation-qt/qthostinterface.cpp +++ b/src/duckstation-qt/qthostinterface.cpp @@ -6,9 +6,9 @@ #include "common/log.h" #include "common/string_util.h" #include "core/controller.h" -#include "core/game_list.h" #include "core/gpu.h" #include "core/system.h" +#include "frontend-common/game_list.h" #include "frontend-common/imgui_styles.h" #include "frontend-common/ini_settings_interface.h" #include "frontend-common/opengl_host_display.h" diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index ddae45acc..a27a9bf2a 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -3,6 +3,10 @@ add_library(frontend-common common_host_interface.h controller_interface.cpp controller_interface.h + game_list.cpp + game_list.h + game_settings.cpp + game_settings.h icon.cpp icon.h imgui_styles.cpp @@ -17,7 +21,7 @@ add_library(frontend-common vulkan_host_display.h ) -target_link_libraries(frontend-common PUBLIC core common imgui simpleini scmversion glad vulkan-loader) +target_link_libraries(frontend-common PUBLIC core common imgui simpleini tinyxml2 scmversion glad vulkan-loader) if(WIN32) target_sources(frontend-common PRIVATE diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 84f9a13fc..93a332774 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -9,7 +9,6 @@ #include "core/cdrom.h" #include "core/cpu_code_cache.h" #include "core/dma.h" -#include "core/game_list.h" #include "core/gpu.h" #include "core/host_display.h" #include "core/mdec.h" @@ -18,6 +17,7 @@ #include "core/spu.h" #include "core/system.h" #include "core/timers.h" +#include "game_list.h" #include "imgui.h" #include "ini_settings_interface.h" #include "save_state_selector_ui.h" @@ -368,7 +368,7 @@ bool CommonHostInterface::ParseCommandLineParameters(int argc, char* argv[], else { // find the game id, and get its save state path - std::string game_code = m_game_list->GetGameCodeForPath(boot_filename.c_str()); + std::string game_code = System::GetGameCodeForPath(boot_filename.c_str()); if (game_code.empty()) { Log_WarningPrintf("Could not identify game code for '%s', cannot load save state %d.", boot_filename.c_str(), @@ -2032,13 +2032,13 @@ void CommonHostInterface::GetGameInfo(const char* path, CDImage* image, std::str else { if (image) - *code = GameList::GetGameCodeForImage(image); + *code = System::GetGameCodeForImage(image); const GameListDatabaseEntry* db_entry = (!code->empty()) ? m_game_list->GetDatabaseEntryForCode(*code) : nullptr; if (db_entry) *title = db_entry->title; else - *title = GameList::GetTitleForPath(path); + *title = System::GetTitleForPath(path); } } diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index ebcae7ee0..e291fa0e3 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -53,6 +53,9 @@ {3773f4cc-614e-4028-8595-22e08ca649e3} + + {933118a9-68c5-47b4-b151-b03c93961623} + {9c8ddeb0-2b8f-4f5f-ba86-127cdf27f035} @@ -67,6 +70,8 @@ + + @@ -82,6 +87,8 @@ + + @@ -241,7 +248,7 @@ WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -269,7 +276,7 @@ WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default false true @@ -300,7 +307,7 @@ WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -328,7 +335,7 @@ WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default false true @@ -360,7 +367,7 @@ true WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -390,7 +397,7 @@ true WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 @@ -422,7 +429,7 @@ true WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -452,7 +459,7 @@ true WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 diff --git a/src/frontend-common/frontend-common.vcxproj.filters b/src/frontend-common/frontend-common.vcxproj.filters index d84627ceb..5efdfa632 100644 --- a/src/frontend-common/frontend-common.vcxproj.filters +++ b/src/frontend-common/frontend-common.vcxproj.filters @@ -14,6 +14,8 @@ + + @@ -29,6 +31,8 @@ + + diff --git a/src/core/game_list.cpp b/src/frontend-common/game_list.cpp similarity index 81% rename from src/core/game_list.cpp rename to src/frontend-common/game_list.cpp index fef61d38b..58db28b40 100644 --- a/src/core/game_list.cpp +++ b/src/frontend-common/game_list.cpp @@ -1,5 +1,4 @@ #include "game_list.h" -#include "bios.h" #include "common/assert.h" #include "common/byte_stream.h" #include "common/cd_image.h" @@ -8,12 +7,13 @@ #include "common/log.h" #include "common/progress_callback.h" #include "common/string_util.h" -#include "host_interface.h" -#include "settings.h" +#include "core/bios.h" +#include "core/host_interface.h" +#include "core/settings.h" +#include "core/system.h" #include #include #include -#include #include #include #include @@ -36,238 +36,6 @@ const char* GameList::EntryCompatibilityRatingToString(GameListCompatibilityRati return names[static_cast(rating)]; } -std::string GameList::GetGameCodeForPath(const char* image_path) -{ - std::unique_ptr cdi = CDImage::Open(image_path); - if (!cdi) - return {}; - - return GetGameCodeForImage(cdi.get()); -} - -std::string GameList::GetGameCodeForImage(CDImage* cdi) -{ - ISOReader iso; - if (!iso.Open(cdi, 1)) - return {}; - - // Read SYSTEM.CNF - std::vector system_cnf_data; - if (!iso.ReadFile("SYSTEM.CNF", &system_cnf_data)) - return {}; - - // Parse lines - std::vector> lines; - std::pair current_line; - bool reading_value = false; - for (size_t pos = 0; pos < system_cnf_data.size(); pos++) - { - const char ch = static_cast(system_cnf_data[pos]); - if (ch == '\r' || ch == '\n') - { - if (!current_line.first.empty()) - { - lines.push_back(std::move(current_line)); - current_line = {}; - reading_value = false; - } - } - else if (ch == ' ' || (ch >= 0x09 && ch <= 0x0D)) - { - continue; - } - else if (ch == '=' && !reading_value) - { - reading_value = true; - } - else - { - if (reading_value) - current_line.second.push_back(ch); - else - current_line.first.push_back(ch); - } - } - - if (!current_line.first.empty()) - lines.push_back(std::move(current_line)); - - // Find the BOOT line - auto iter = std::find_if(lines.begin(), lines.end(), - [](const auto& it) { return StringUtil::Strcasecmp(it.first.c_str(), "boot") == 0; }); - if (iter == lines.end()) - return {}; - - // cdrom:\SCES_123.45;1 - std::string code = iter->second; - std::string::size_type pos = code.rfind('\\'); - if (pos != std::string::npos) - { - code.erase(0, pos + 1); - } - else - { - // cdrom:SCES_123.45;1 - pos = code.rfind(':'); - if (pos != std::string::npos) - code.erase(0, pos + 1); - } - - pos = code.find(';'); - if (pos != std::string::npos) - code.erase(pos); - - // SCES_123.45 -> SCES-12345 - for (pos = 0; pos < code.size();) - { - if (code[pos] == '.') - { - code.erase(pos, 1); - continue; - } - - if (code[pos] == '_') - code[pos] = '-'; - else - code[pos] = static_cast(std::toupper(code[pos])); - - pos++; - } - - return code; -} - -DiscRegion GameList::GetRegionForCode(std::string_view code) -{ - std::string prefix; - for (size_t pos = 0; pos < code.length(); pos++) - { - const int ch = std::tolower(code[pos]); - if (ch < 'a' || ch > 'z') - break; - - prefix.push_back(static_cast(ch)); - } - - if (prefix == "sces" || prefix == "sced" || prefix == "sles" || prefix == "sled") - return DiscRegion::PAL; - else if (prefix == "scps" || prefix == "slps" || prefix == "slpm" || prefix == "sczs" || prefix == "papx") - return DiscRegion::NTSC_J; - else if (prefix == "scus" || prefix == "slus") - return DiscRegion::NTSC_U; - else - return DiscRegion::Other; -} - -DiscRegion GameList::GetRegionFromSystemArea(CDImage* cdi) -{ - // The license code is on sector 4 of the disc. - u8 sector[CDImage::DATA_SECTOR_SIZE]; - if (!cdi->Seek(1, 4) || cdi->Read(CDImage::ReadMode::DataOnly, 1, sector) != 1) - return DiscRegion::Other; - - static constexpr char ntsc_u_string[] = " Licensed by Sony Computer Entertainment Amer ica "; - static constexpr char ntsc_j_string[] = " Licensed by Sony Computer Entertainment Inc."; - static constexpr char pal_string[] = " Licensed by Sony Computer Entertainment Euro pe"; - - // subtract one for the terminating null - if (std::equal(ntsc_u_string, ntsc_u_string + countof(ntsc_u_string) - 1, sector)) - return DiscRegion::NTSC_U; - else if (std::equal(ntsc_j_string, ntsc_j_string + countof(ntsc_j_string) - 1, sector)) - return DiscRegion::NTSC_J; - else if (std::equal(pal_string, pal_string + countof(pal_string) - 1, sector)) - return DiscRegion::PAL; - else - return DiscRegion::Other; -} - -DiscRegion GameList::GetRegionForImage(CDImage* cdi) -{ - DiscRegion system_area_region = GetRegionFromSystemArea(cdi); - if (system_area_region != DiscRegion::Other) - return system_area_region; - - std::string code = GetGameCodeForImage(cdi); - if (code.empty()) - return DiscRegion::Other; - - return GetRegionForCode(code); -} - -std::optional GameList::GetRegionForPath(const char* image_path) -{ - std::unique_ptr cdi = CDImage::Open(image_path); - if (!cdi) - return {}; - - return GetRegionForImage(cdi.get()); -} - -bool GameList::IsExeFileName(const char* path) -{ - const char* extension = std::strrchr(path, '.'); - return (extension && - (StringUtil::Strcasecmp(extension, ".exe") == 0 || StringUtil::Strcasecmp(extension, ".psexe") == 0)); -} - -bool GameList::IsPsfFileName(const char* path) -{ - const char* extension = std::strrchr(path, '.'); - return (extension && StringUtil::Strcasecmp(extension, ".psf") == 0); -} - -bool GameList::IsM3UFileName(const char* path) -{ - const char* extension = std::strrchr(path, '.'); - return (extension && StringUtil::Strcasecmp(extension, ".m3u") == 0); -} - -std::vector GameList::ParseM3UFile(const char* path) -{ - std::ifstream ifs(path); - if (!ifs.is_open()) - { - Log_ErrorPrintf("Failed to open %s", path); - return {}; - } - - std::vector entries; - std::string line; - while (std::getline(ifs, line)) - { - u32 start_offset = 0; - while (start_offset < line.size() && std::isspace(line[start_offset])) - start_offset++; - - // skip comments - if (start_offset == line.size() || line[start_offset] == '#') - continue; - - // strip ending whitespace - u32 end_offset = static_cast(line.size()) - 1; - while (std::isspace(line[end_offset]) && end_offset > start_offset) - end_offset--; - - // anything? - if (start_offset == end_offset) - continue; - - std::string entry_path(line.begin() + start_offset, line.begin() + end_offset + 1); - if (!FileSystem::IsAbsolutePath(entry_path)) - { - SmallString absolute_path; - FileSystem::BuildPathRelativeToFile(absolute_path, path, entry_path.c_str()); - entry_path = absolute_path; - } - - Log_DevPrintf("Read path from m3u: '%s'", entry_path.c_str()); - entries.push_back(std::move(entry_path)); - } - - Log_InfoPrintf("Loaded %zu paths from m3u '%s'", entries.size(), path); - return entries; -} - const char* GameList::GetGameListCompatibilityRatingString(GameListCompatibilityRating rating) { static constexpr std::array(GameListCompatibilityRating::Count)> names = { @@ -292,21 +60,6 @@ static std::string_view GetFileNameFromPath(const char* path) return std::string_view(filename_start + 1, filename_end - filename_start); } -std::string_view GameList::GetTitleForPath(const char* path) -{ - const char* extension = std::strrchr(path, '.'); - if (path == extension) - return path; - - const char* path_end = path + std::strlen(path); - const char* title_end = extension ? (extension - 1) : (path_end); - const char* title_start = std::max(std::strrchr(path, '/'), std::strrchr(path, '\\')); - if (!title_start || title_start == path) - return std::string_view(path, title_end - title_start); - else - return std::string_view(title_start + 1, title_end - title_start); -} - bool GameList::GetExeListEntry(const char* path, GameListEntry* entry) { FILESYSTEM_STAT_DATA ffd; @@ -360,12 +113,12 @@ bool GameList::GetM3UListEntry(const char* path, GameListEntry* entry) if (!FileSystem::StatFile(path, &ffd)) return false; - std::vector entries = ParseM3UFile(path); + std::vector entries = System::ParseM3UFile(path); if (entries.empty()) return false; entry->code.clear(); - entry->title = GetTitleForPath(path); + entry->title = System::GetTitleForPath(path); entry->path = path; entry->region = DiscRegion::Other; entry->total_size = 0; @@ -385,11 +138,11 @@ bool GameList::GetM3UListEntry(const char* path, GameListEntry* entry) entry->total_size += static_cast(CDImage::RAW_SECTOR_SIZE) * static_cast(entry_image->GetLBACount()); if (entry->region == DiscRegion::Other) - entry->region = GetRegionForImage(entry_image.get()); + entry->region = System::GetRegionForImage(entry_image.get()); if (entry->compatibility_rating == GameListCompatibilityRating::Unknown) { - std::string code = GetGameCodeForImage(entry_image.get()); + std::string code = System::GetGameCodeForImage(entry_image.get()); const GameListCompatibilityEntry* compatibility_entry = GetCompatibilityEntryForCode(entry->code); if (compatibility_entry) entry->compatibility_rating = compatibility_entry->compatibility_rating; @@ -403,19 +156,19 @@ bool GameList::GetM3UListEntry(const char* path, GameListEntry* entry) bool GameList::GetGameListEntry(const std::string& path, GameListEntry* entry) { - if (IsExeFileName(path.c_str())) + if (System::IsExeFileName(path.c_str())) return GetExeListEntry(path.c_str(), entry); - if (IsM3UFileName(path.c_str())) + if (System::IsM3UFileName(path.c_str())) return GetM3UListEntry(path.c_str(), entry); std::unique_ptr cdi = CDImage::Open(path.c_str()); if (!cdi) return false; - std::string code = GetGameCodeForImage(cdi.get()); - DiscRegion region = GetRegionFromSystemArea(cdi.get()); + std::string code = System::GetGameCodeForImage(cdi.get()); + DiscRegion region = System::GetRegionFromSystemArea(cdi.get()); if (region == DiscRegion::Other) - region = GetRegionForCode(code); + region = System::GetRegionForCode(code); entry->path = path; entry->code = std::move(code); @@ -428,7 +181,7 @@ bool GameList::GetGameListEntry(const std::string& path, GameListEntry* entry) if (entry->code.empty()) { // no game code, so use the filename title - entry->title = GetTitleForPath(path.c_str()); + entry->title = System::GetTitleForPath(path.c_str()); entry->compatibility_rating = GameListCompatibilityRating::Unknown; } else @@ -444,7 +197,7 @@ bool GameList::GetGameListEntry(const std::string& path, GameListEntry* entry) else { Log_WarningPrintf("'%s' not found in database", entry->code.c_str()); - entry->title = GetTitleForPath(path.c_str()); + entry->title = System::GetTitleForPath(path.c_str()); } const GameListCompatibilityEntry* compatibility_entry = GetCompatibilityEntryForCode(entry->code); @@ -825,7 +578,7 @@ public: { GameListDatabaseEntry gde; gde.code = std::move(code); - gde.region = GameList::GetRegionForCode(gde.code); + gde.region = System::GetRegionForCode(gde.code); gde.title = name; m_database.emplace(gde.code, std::move(gde)); } diff --git a/src/core/game_list.h b/src/frontend-common/game_list.h similarity index 85% rename from src/core/game_list.h rename to src/frontend-common/game_list.h index 7b4f3fc80..22503f306 100644 --- a/src/core/game_list.h +++ b/src/frontend-common/game_list.h @@ -1,6 +1,6 @@ #pragma once +#include "core/types.h" #include "game_settings.h" -#include "types.h" #include #include #include @@ -74,29 +74,9 @@ public: static const char* EntryTypeToString(GameListEntryType type); static const char* EntryCompatibilityRatingToString(GameListCompatibilityRating rating); - /// Returns true if the filename is a PlayStation executable we can inject. - static bool IsExeFileName(const char* path); - - /// Returns true if the filename is a Portable Sound Format file we can uncompress/load. - static bool IsPsfFileName(const char* path); - - /// Returns true if the filename is a M3U Playlist we can handle. - static bool IsM3UFileName(const char* path); - - /// Parses an M3U playlist, returning the entries. - static std::vector ParseM3UFile(const char* path); - /// Returns a string representation of a compatibility level. static const char* GetGameListCompatibilityRatingString(GameListCompatibilityRating rating); - static std::string GetGameCodeForImage(CDImage* cdi); - static std::string GetGameCodeForPath(const char* image_path); - static DiscRegion GetRegionForCode(std::string_view code); - static DiscRegion GetRegionFromSystemArea(CDImage* cdi); - static DiscRegion GetRegionForImage(CDImage* cdi); - static std::optional GetRegionForPath(const char* image_path); - static std::string_view GetTitleForPath(const char* path); - const EntryList& GetEntries() const { return m_entries; } const u32 GetEntryCount() const { return static_cast(m_entries.size()); } diff --git a/src/core/game_settings.cpp b/src/frontend-common/game_settings.cpp similarity index 99% rename from src/core/game_settings.cpp rename to src/frontend-common/game_settings.cpp index 4e347f9e4..f390398d2 100644 --- a/src/core/game_settings.cpp +++ b/src/frontend-common/game_settings.cpp @@ -5,8 +5,8 @@ #include "common/log.h" #include "common/string.h" #include "common/string_util.h" -#include "host_interface.h" -#include "settings.h" +#include "core/host_interface.h" +#include "core/settings.h" #include #include Log_SetChannel(GameSettings); diff --git a/src/core/game_settings.h b/src/frontend-common/game_settings.h similarity index 97% rename from src/core/game_settings.h rename to src/frontend-common/game_settings.h index 030e957e1..918210ab1 100644 --- a/src/core/game_settings.h +++ b/src/frontend-common/game_settings.h @@ -1,5 +1,5 @@ #pragma once -#include "types.h" +#include "core/types.h" #include #include #include From 0af22825ad0370215460c3519514a8f0ab65d7d2 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 1 Sep 2020 12:46:58 +1000 Subject: [PATCH 43/61] FrontendCommon: Drop imgui deps for libretro build --- src/duckstation-libretro/CMakeLists.txt | 2 +- src/frontend-common/CMakeLists.txt | 98 ++++++++++++--------- src/frontend-common/d3d11_host_display.cpp | 26 +++--- src/frontend-common/d3d11_host_display.h | 5 +- src/frontend-common/frontend-common.vcxproj | 16 ++-- src/frontend-common/opengl_host_display.cpp | 24 ++++- src/frontend-common/vulkan_host_display.cpp | 23 ++++- 7 files changed, 126 insertions(+), 68 deletions(-) diff --git a/src/duckstation-libretro/CMakeLists.txt b/src/duckstation-libretro/CMakeLists.txt index 804a7ba21..9638e35ee 100644 --- a/src/duckstation-libretro/CMakeLists.txt +++ b/src/duckstation-libretro/CMakeLists.txt @@ -21,7 +21,7 @@ if(WIN32) ) endif() -target_link_libraries(duckstation_libretro PRIVATE core common imgui glad scmversion frontend-common vulkan-loader libretro-common) +target_link_libraries(duckstation_libretro PRIVATE core common glad scmversion frontend-common vulkan-loader libretro-common) # no lib prefix set_target_properties(duckstation_libretro PROPERTIES PREFIX "") diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index a27a9bf2a..b92984c92 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -1,64 +1,78 @@ add_library(frontend-common - common_host_interface.cpp - common_host_interface.h - controller_interface.cpp - controller_interface.h - game_list.cpp - game_list.h - game_settings.cpp - game_settings.h - icon.cpp - icon.h - imgui_styles.cpp - imgui_styles.h - ini_settings_interface.cpp - ini_settings_interface.h opengl_host_display.cpp opengl_host_display.h - save_state_selector_ui.cpp - save_state_selector_ui.h vulkan_host_display.cpp vulkan_host_display.h ) -target_link_libraries(frontend-common PUBLIC core common imgui simpleini tinyxml2 scmversion glad vulkan-loader) +target_link_libraries(frontend-common PUBLIC core common glad vulkan-loader) if(WIN32) target_sources(frontend-common PRIVATE d3d11_host_display.cpp d3d11_host_display.h - xinput_controller_interface.cpp - xinput_controller_interface.h ) target_link_libraries(frontend-common PRIVATE d3d11.lib dxgi.lib) endif() -if(SDL2_FOUND AND NOT BUILD_LIBRETRO_CORE) - target_sources(frontend-common PRIVATE - sdl_audio_stream.cpp - sdl_audio_stream.h - sdl_controller_interface.cpp - sdl_controller_interface.h - sdl_initializer.cpp - sdl_initializer.h +if(NOT BUILD_LIBRETRO_CORE) + target_sources(frontend-common PRIVATE + common_host_interface.cpp + common_host_interface.h + controller_interface.cpp + controller_interface.h + game_list.cpp + game_list.h + game_settings.cpp + game_settings.h + icon.cpp + icon.h + imgui_styles.cpp + imgui_styles.h + ini_settings_interface.cpp + ini_settings_interface.h + save_state_selector_ui.cpp + save_state_selector_ui.h ) - target_compile_definitions(frontend-common PUBLIC "WITH_SDL2=1") - target_include_directories(frontend-common PRIVATE ${SDL2_INCLUDE_DIRS}) - target_link_libraries(frontend-common PRIVATE ${SDL2_LIBRARIES}) - # Copy bundled SDL2 to output on Windows. if(WIN32) - add_custom_command(TARGET frontend-common POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${SDL2_DLL_PATH}" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/SDL2.dll") + target_sources(frontend-common PRIVATE + xinput_controller_interface.cpp + xinput_controller_interface.h + ) endif() + + target_compile_definitions(frontend-common PRIVATE "WITH_IMGUI=1") + target_link_libraries(frontend-common PUBLIC imgui simpleini tinyxml2 scmversion) + + if(SDL2_FOUND) + target_sources(frontend-common PRIVATE + sdl_audio_stream.cpp + sdl_audio_stream.h + sdl_controller_interface.cpp + sdl_controller_interface.h + sdl_initializer.cpp + sdl_initializer.h + ) + target_compile_definitions(frontend-common PUBLIC "WITH_SDL2=1") + target_include_directories(frontend-common PRIVATE ${SDL2_INCLUDE_DIRS}) + target_link_libraries(frontend-common PRIVATE ${SDL2_LIBRARIES}) + + # Copy bundled SDL2 to output on Windows. + if(WIN32) + add_custom_command(TARGET frontend-common POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${SDL2_DLL_PATH}" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/SDL2.dll") + endif() + endif() + + if(ENABLE_DISCORD_PRESENCE AND NOT BUILD_LIBRETRO_CORE) + target_compile_definitions(frontend-common PUBLIC -DWITH_DISCORD_PRESENCE=1) + target_link_libraries(frontend-common PRIVATE discord-rpc) + endif() + + # Copy the provided data directory to the output directory. + add_custom_command(TARGET frontend-common POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/data" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" + ) endif() -if(ENABLE_DISCORD_PRESENCE AND NOT BUILD_LIBRETRO_CORE) - target_compile_definitions(frontend-common PUBLIC -DWITH_DISCORD_PRESENCE=1) - target_link_libraries(frontend-common PRIVATE discord-rpc) -endif() - -# Copy the provided data directory to the output directory. -add_custom_command(TARGET frontend-common POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/data" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" -) diff --git a/src/frontend-common/d3d11_host_display.cpp b/src/frontend-common/d3d11_host_display.cpp index e4d9ce613..b8ec7fe58 100644 --- a/src/frontend-common/d3d11_host_display.cpp +++ b/src/frontend-common/d3d11_host_display.cpp @@ -8,8 +8,10 @@ #include #ifndef LIBRETRO #include -#include -#include +#endif +#ifdef WITH_IMGUI +#include "imgui.h" +#include "imgui_impl_dx11.h" #endif Log_SetChannel(D3D11HostDisplay); @@ -298,7 +300,7 @@ bool D3D11HostDisplay::InitializeRenderDevice(std::string_view shader_cache_dire if (!CreateResources()) return false; -#ifndef LIBRETRO +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext() && !CreateImGuiContext()) return false; #endif @@ -308,7 +310,7 @@ bool D3D11HostDisplay::InitializeRenderDevice(std::string_view shader_cache_dire void D3D11HostDisplay::DestroyRenderDevice() { -#ifndef LIBRETRO +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) DestroyImGuiContext(); #endif @@ -532,9 +534,9 @@ void D3D11HostDisplay::DestroyResources() m_display_rasterizer_state.Reset(); } -#ifndef LIBRETRO bool D3D11HostDisplay::CreateImGuiContext() { +#ifdef WITH_IMGUI ImGui::GetIO().DisplaySize.x = static_cast(m_window_info.surface_width); ImGui::GetIO().DisplaySize.y = static_cast(m_window_info.surface_height); @@ -542,14 +544,16 @@ bool D3D11HostDisplay::CreateImGuiContext() return false; ImGui_ImplDX11_NewFrame(); +#endif return true; } void D3D11HostDisplay::DestroyImGuiContext() { +#ifdef WITH_IMGUI ImGui_ImplDX11_Shutdown(); -} #endif +} bool D3D11HostDisplay::Render() { @@ -560,8 +564,10 @@ bool D3D11HostDisplay::Render() RenderDisplay(); +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) RenderImGui(); +#endif RenderSoftwareCursor(); @@ -570,8 +576,10 @@ bool D3D11HostDisplay::Render() else m_swap_chain->Present(BoolToUInt32(m_vsync), 0); +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) ImGui_ImplDX11_NewFrame(); +#endif #else RenderDisplay(); RenderSoftwareCursor(); @@ -580,15 +588,13 @@ bool D3D11HostDisplay::Render() return true; } -#ifndef LIBRETRO - void D3D11HostDisplay::RenderImGui() { +#ifdef WITH_IMGUI ImGui::Render(); ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); -} - #endif +} void D3D11HostDisplay::RenderDisplay() { diff --git a/src/frontend-common/d3d11_host_display.h b/src/frontend-common/d3d11_host_display.h index b10d8ed15..e992c8ac4 100644 --- a/src/frontend-common/d3d11_host_display.h +++ b/src/frontend-common/d3d11_host_display.h @@ -65,20 +65,17 @@ protected: virtual bool CreateResources(); virtual void DestroyResources(); -#ifndef LIBRETRO virtual bool CreateImGuiContext(); virtual void DestroyImGuiContext(); +#ifndef LIBRETRO bool CreateSwapChain(); bool CreateSwapChainRTV(); #endif void RenderDisplay(); void RenderSoftwareCursor(); - -#ifndef LIBRETRO void RenderImGui(); -#endif void RenderDisplay(s32 left, s32 top, s32 width, s32 height, void* texture_handle, u32 texture_width, s32 texture_height, s32 texture_view_x, s32 texture_view_y, s32 texture_view_width, diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index e291fa0e3..8cfe596ea 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -245,7 +245,7 @@ Level3 Disabled - WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -273,7 +273,7 @@ Level3 Disabled - WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -304,7 +304,7 @@ Level3 Disabled - WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -332,7 +332,7 @@ Level3 Disabled - WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -365,7 +365,7 @@ MaxSpeed true true - WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true @@ -395,7 +395,7 @@ MaxSpeed true true - WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true @@ -427,7 +427,7 @@ MaxSpeed true true - WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true @@ -457,7 +457,7 @@ MaxSpeed true true - WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true diff --git a/src/frontend-common/opengl_host_display.cpp b/src/frontend-common/opengl_host_display.cpp index 69f10faaf..200fe7433 100644 --- a/src/frontend-common/opengl_host_display.cpp +++ b/src/frontend-common/opengl_host_display.cpp @@ -1,10 +1,12 @@ #include "opengl_host_display.h" #include "common/assert.h" #include "common/log.h" -#include "imgui.h" #include -#include #include +#ifdef WITH_IMGUI +#include "imgui.h" +#include "imgui_impl_opengl3.h" +#endif Log_SetChannel(LibretroOpenGLHostDisplay); namespace FrontendCommon { @@ -219,8 +221,10 @@ bool OpenGLHostDisplay::InitializeRenderDevice(std::string_view shader_cache_dir if (!CreateResources()) return false; +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext() && !CreateImGuiContext()) return false; +#endif return true; } @@ -246,8 +250,10 @@ void OpenGLHostDisplay::DestroyRenderDevice() if (!m_gl_context) return; +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) DestroyImGuiContext(); +#endif DestroyResources(); @@ -269,11 +275,13 @@ bool OpenGLHostDisplay::ChangeRenderWindow(const WindowInfo& new_wi) m_window_info.surface_width = m_gl_context->GetSurfaceWidth(); m_window_info.surface_height = m_gl_context->GetSurfaceHeight(); +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) { ImGui::GetIO().DisplaySize.x = static_cast(m_window_info.surface_width); ImGui::GetIO().DisplaySize.y = static_cast(m_window_info.surface_height); } +#endif return true; } @@ -287,11 +295,13 @@ void OpenGLHostDisplay::ResizeRenderWindow(s32 new_window_width, s32 new_window_ m_window_info.surface_width = m_gl_context->GetSurfaceWidth(); m_window_info.surface_height = m_gl_context->GetSurfaceHeight(); +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) { ImGui::GetIO().DisplaySize.x = static_cast(m_window_info.surface_width); ImGui::GetIO().DisplaySize.y = static_cast(m_window_info.surface_height); } +#endif } void OpenGLHostDisplay::DestroyRenderSurface() @@ -306,6 +316,7 @@ void OpenGLHostDisplay::DestroyRenderSurface() bool OpenGLHostDisplay::CreateImGuiContext() { +#ifdef WITH_IMGUI ImGui::GetIO().DisplaySize.x = static_cast(m_window_info.surface_width); ImGui::GetIO().DisplaySize.y = static_cast(m_window_info.surface_height); @@ -313,12 +324,15 @@ bool OpenGLHostDisplay::CreateImGuiContext() return false; ImGui_ImplOpenGL3_NewFrame(); +#endif return true; } void OpenGLHostDisplay::DestroyImGuiContext() { +#ifdef WITH_IMGUI ImGui_ImplOpenGL3_Shutdown(); +#endif } bool OpenGLHostDisplay::CreateResources() @@ -424,24 +438,30 @@ bool OpenGLHostDisplay::Render() RenderDisplay(); +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) RenderImGui(); +#endif RenderSoftwareCursor(); m_gl_context->SwapBuffers(); +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) ImGui_ImplOpenGL3_NewFrame(); +#endif return true; } void OpenGLHostDisplay::RenderImGui() { +#ifdef WITH_IMGUI ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); GL::Program::ResetLastProgram(); +#endif } void OpenGLHostDisplay::RenderDisplay() diff --git a/src/frontend-common/vulkan_host_display.cpp b/src/frontend-common/vulkan_host_display.cpp index e90965bbd..971c80876 100644 --- a/src/frontend-common/vulkan_host_display.cpp +++ b/src/frontend-common/vulkan_host_display.cpp @@ -9,9 +9,11 @@ #include "common/vulkan/stream_buffer.h" #include "common/vulkan/swap_chain.h" #include "common/vulkan/util.h" +#include +#ifdef WITH_IMGUI #include "imgui.h" #include "imgui_impl_vulkan.h" -#include +#endif Log_SetChannel(VulkanHostDisplay); namespace FrontendCommon { @@ -134,11 +136,13 @@ bool VulkanHostDisplay::ChangeRenderWindow(const WindowInfo& new_wi) m_window_info.surface_width = m_swap_chain->GetWidth(); m_window_info.surface_height = m_swap_chain->GetHeight(); +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) { ImGui::GetIO().DisplaySize.x = static_cast(m_window_info.surface_width); ImGui::GetIO().DisplaySize.y = static_cast(m_window_info.surface_height); } +#endif return true; } @@ -153,11 +157,13 @@ void VulkanHostDisplay::ResizeRenderWindow(s32 new_window_width, s32 new_window_ m_window_info.surface_width = m_swap_chain->GetWidth(); m_window_info.surface_height = m_swap_chain->GetHeight(); +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) { ImGui::GetIO().DisplaySize.x = static_cast(m_window_info.surface_width); ImGui::GetIO().DisplaySize.y = static_cast(m_window_info.surface_height); } +#endif } void VulkanHostDisplay::DestroyRenderSurface() @@ -250,8 +256,10 @@ bool VulkanHostDisplay::InitializeRenderDevice(std::string_view shader_cache_dir if (!CreateResources()) return false; +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext() && !CreateImGuiContext()) return false; +#endif return true; } @@ -400,7 +408,9 @@ void VulkanHostDisplay::DestroyResources() void VulkanHostDisplay::DestroyImGuiContext() { +#ifdef WITH_IMGUI ImGui_ImplVulkan_Shutdown(); +#endif } void VulkanHostDisplay::DestroyRenderDevice() @@ -410,8 +420,10 @@ void VulkanHostDisplay::DestroyRenderDevice() g_vulkan_context->WaitForGPUIdle(); +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) DestroyImGuiContext(); +#endif DestroyResources(); @@ -432,6 +444,7 @@ bool VulkanHostDisplay::DoneRenderContextCurrent() bool VulkanHostDisplay::CreateImGuiContext() { +#ifdef WITH_IMGUI ImGui::GetIO().DisplaySize.x = static_cast(m_window_info.surface_width); ImGui::GetIO().DisplaySize.y = static_cast(m_window_info.surface_height); @@ -454,6 +467,8 @@ bool VulkanHostDisplay::CreateImGuiContext() } ImGui_ImplVulkan_NewFrame(); +#endif + return true; } @@ -499,8 +514,10 @@ bool VulkanHostDisplay::Render() RenderDisplay(); +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) RenderImGui(); +#endif RenderSoftwareCursor(); @@ -513,8 +530,10 @@ bool VulkanHostDisplay::Render() m_swap_chain->GetCurrentImageIndex()); g_vulkan_context->MoveToNextCommandBuffer(); +#ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) ImGui_ImplVulkan_NewFrame(); +#endif return true; } @@ -565,8 +584,10 @@ void VulkanHostDisplay::RenderDisplay(s32 left, s32 top, s32 width, s32 height, void VulkanHostDisplay::RenderImGui() { +#ifdef WITH_IMGUI ImGui::Render(); ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), g_vulkan_context->GetCurrentCommandBuffer()); +#endif } void VulkanHostDisplay::RenderSoftwareCursor() From bf85fbe331c9a6998d0de1917626cbfebeb1d1e2 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 1 Sep 2020 20:29:15 +1000 Subject: [PATCH 44/61] GameSettings: Fix widescreen hack not saving to cache --- src/frontend-common/game_list.h | 2 +- src/frontend-common/game_settings.cpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/frontend-common/game_list.h b/src/frontend-common/game_list.h index 22503f306..aac50561d 100644 --- a/src/frontend-common/game_list.h +++ b/src/frontend-common/game_list.h @@ -112,7 +112,7 @@ private: enum : u32 { GAME_LIST_CACHE_SIGNATURE = 0x45434C47, - GAME_LIST_CACHE_VERSION = 6 + GAME_LIST_CACHE_VERSION = 7 }; using DatabaseMap = std::unordered_map; diff --git a/src/frontend-common/game_settings.cpp b/src/frontend-common/game_settings.cpp index f390398d2..7b2bc84af 100644 --- a/src/frontend-common/game_settings.cpp +++ b/src/frontend-common/game_settings.cpp @@ -91,7 +91,8 @@ bool Entry::LoadFromStream(ByteStream* stream) if (!stream->Read2(bits.data(), num_bytes) || !ReadOptionalFromStream(stream, &display_active_start_offset) || !ReadOptionalFromStream(stream, &display_active_end_offset) || !ReadOptionalFromStream(stream, &display_crop_mode) || !ReadOptionalFromStream(stream, &display_aspect_ratio) || - !ReadOptionalFromStream(stream, &controller_1_type) || !ReadOptionalFromStream(stream, &controller_2_type)) + !ReadOptionalFromStream(stream, &controller_1_type) || !ReadOptionalFromStream(stream, &controller_2_type) || + !ReadOptionalFromStream(stream, &gpu_widescreen_hack)) { return false; } @@ -120,7 +121,7 @@ bool Entry::SaveToStream(ByteStream* stream) const return stream->Write2(bits.data(), num_bytes) && WriteOptionalToStream(stream, display_active_start_offset) && WriteOptionalToStream(stream, display_active_end_offset) && WriteOptionalToStream(stream, display_crop_mode) && WriteOptionalToStream(stream, display_aspect_ratio) && WriteOptionalToStream(stream, controller_1_type) && - WriteOptionalToStream(stream, controller_2_type); + WriteOptionalToStream(stream, controller_2_type) && WriteOptionalToStream(stream, gpu_widescreen_hack); } static void ParseIniSection(Entry* entry, const char* section, const CSimpleIniA& ini) From c9cefe402073de94cd69f7a5cf6adb4043d56d9c Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Wed, 2 Sep 2020 00:00:48 +1000 Subject: [PATCH 45/61] GameSettings: Add a bunch more user settings --- README.md | 1 + src/duckstation-qt/gamepropertiesdialog.cpp | 148 +++++++++++++-- src/duckstation-qt/gamepropertiesdialog.h | 2 + src/duckstation-qt/gamepropertiesdialog.ui | 200 ++++++++++++++++++-- src/duckstation-qt/gpusettingswidget.cpp | 69 +++---- src/duckstation-qt/qtutils.cpp | 23 +++ src/duckstation-qt/qtutils.h | 4 + src/frontend-common/game_list.h | 2 +- src/frontend-common/game_settings.cpp | 151 ++++++++++++++- src/frontend-common/game_settings.h | 14 +- 10 files changed, 531 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 56e4bd7cf..db538a896 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A "BIOS" ROM image is required to to start the emulator and to play games. You c ## Latest News +- 2020/09/01: Many additional user settings available, including memory cards and enhancements. Now you can set these per-game. - 2020/08/25: Automated builds for macOS now available. - 2020/08/22: XInput controller backend added. - 2020/08/20: Per-game setting overrides added. Mostly for compatibility, but some options are customizable. diff --git a/src/duckstation-qt/gamepropertiesdialog.cpp b/src/duckstation-qt/gamepropertiesdialog.cpp index 1564b54ca..b595ac76a 100644 --- a/src/duckstation-qt/gamepropertiesdialog.cpp +++ b/src/duckstation-qt/gamepropertiesdialog.cpp @@ -9,9 +9,13 @@ #include "scmversion/scmversion.h" #include #include +#include #include #include +static constexpr char MEMORY_CARD_IMAGE_FILTER[] = + QT_TRANSLATE_NOOP("MemoryCardSettingsWidget", "All Memory Card Types (*.mcd *.mcr *.mc)"); + GamePropertiesDialog::GamePropertiesDialog(QtHostInterface* host_interface, QWidget* parent /* = nullptr */) : QDialog(parent), m_host_interface(host_interface) { @@ -127,13 +131,15 @@ void GamePropertiesDialog::setupAdditionalUi() qApp->translate("DisplayCropMode", Settings::GetDisplayCropModeDisplayName(static_cast(i)))); } + m_ui.userResolutionScale->addItem(tr("(unchanged)")); + QtUtils::FillComboBoxWithResolutionScales(m_ui.userResolutionScale); + m_ui.userControllerType1->addItem(tr("(unchanged)")); for (u32 i = 0; i < static_cast(ControllerType::Count); i++) { m_ui.userControllerType1->addItem( qApp->translate("ControllerType", Settings::GetControllerTypeDisplayName(static_cast(i)))); } - m_ui.userControllerType2->addItem(tr("(unchanged)")); for (u32 i = 0; i < static_cast(ControllerType::Count); i++) { @@ -141,6 +147,19 @@ void GamePropertiesDialog::setupAdditionalUi() qApp->translate("ControllerType", Settings::GetControllerTypeDisplayName(static_cast(i)))); } + m_ui.userMemoryCard1Type->addItem(tr("(unchanged)")); + for (u32 i = 0; i < static_cast(MemoryCardType::Count); i++) + { + m_ui.userMemoryCard1Type->addItem( + qApp->translate("MemoryCardType", Settings::GetMemoryCardTypeDisplayName(static_cast(i)))); + } + m_ui.userMemoryCard2Type->addItem(tr("(unchanged)")); + for (u32 i = 0; i < static_cast(MemoryCardType::Count); i++) + { + m_ui.userMemoryCard2Type->addItem( + qApp->translate("MemoryCardType", Settings::GetMemoryCardTypeDisplayName(static_cast(i)))); + } + QGridLayout* traits_layout = new QGridLayout(m_ui.compatibilityTraits); for (u32 i = 0; i < static_cast(GameSettings::Trait::Count); i++) { @@ -198,6 +217,26 @@ void GamePropertiesDialog::populateTracksInfo(const std::string& image_path) } } +void GamePropertiesDialog::populateBooleanUserSetting(QCheckBox* cb, const std::optional& value) +{ + QSignalBlocker sb(cb); + if (value.has_value()) + cb->setCheckState(value.value() ? Qt::Checked : Qt::Unchecked); + else + cb->setCheckState(Qt::PartiallyChecked); +} + +void GamePropertiesDialog::connectBooleanUserSetting(QCheckBox* cb, std::optional* value) +{ + connect(cb, &QCheckBox::stateChanged, [this, value](int state) { + if (state == Qt::PartiallyChecked) + value->reset(); + else + *value = (state == Qt::Checked); + saveGameSettings(); + }); +} + void GamePropertiesDialog::populateGameSettings() { const GameSettings::Entry& gs = m_game_settings; @@ -230,6 +269,27 @@ void GamePropertiesDialog::populateGameSettings() m_ui.userAspectRatio->setCurrentIndex(static_cast(gs.display_aspect_ratio.value()) + 1); } + populateBooleanUserSetting(m_ui.userLinearUpscaling, gs.display_linear_upscaling); + populateBooleanUserSetting(m_ui.userIntegerUpscaling, gs.display_integer_upscaling); + + if (gs.gpu_resolution_scale.has_value()) + { + QSignalBlocker sb(m_ui.userResolutionScale); + m_ui.userResolutionScale->setCurrentIndex(static_cast(gs.gpu_resolution_scale.value()) + 1); + } + else + { + QSignalBlocker sb(m_ui.userResolutionScale); + m_ui.userResolutionScale->setCurrentIndex(0); + } + + populateBooleanUserSetting(m_ui.userTrueColor, gs.gpu_true_color); + populateBooleanUserSetting(m_ui.userScaledDithering, gs.gpu_scaled_dithering); + populateBooleanUserSetting(m_ui.userBilinearTextureFiltering, gs.gpu_bilinear_texture_filtering); + populateBooleanUserSetting(m_ui.userForceNTSCTimings, gs.gpu_force_ntsc_timings); + populateBooleanUserSetting(m_ui.userWidescreenHack, gs.gpu_widescreen_hack); + populateBooleanUserSetting(m_ui.userPGXP, gs.gpu_pgxp); + if (gs.controller_1_type.has_value()) { QSignalBlocker sb(m_ui.userControllerType1); @@ -240,15 +300,26 @@ void GamePropertiesDialog::populateGameSettings() QSignalBlocker sb(m_ui.userControllerType2); m_ui.userControllerType2->setCurrentIndex(static_cast(gs.controller_2_type.value()) + 1); } - if (gs.gpu_widescreen_hack.has_value()) + + if (gs.memory_card_1_type.has_value()) { - QSignalBlocker sb(m_ui.userWidescreenHack); - m_ui.userWidescreenHack->setCheckState(gs.gpu_widescreen_hack.value() ? Qt::Checked : Qt::Unchecked); + QSignalBlocker sb(m_ui.userMemoryCard1Type); + m_ui.userMemoryCard1Type->setCurrentIndex(static_cast(gs.memory_card_1_type.value()) + 1); } - else + if (gs.memory_card_2_type.has_value()) { - QSignalBlocker sb(m_ui.userWidescreenHack); - m_ui.userWidescreenHack->setCheckState(Qt::PartiallyChecked); + QSignalBlocker sb(m_ui.userMemoryCard2Type); + m_ui.userMemoryCard2Type->setCurrentIndex(static_cast(gs.memory_card_2_type.value()) + 1); + } + if (!gs.memory_card_1_shared_path.empty()) + { + QSignalBlocker sb(m_ui.userMemoryCard1SharedPath); + m_ui.userMemoryCard1SharedPath->setText(QString::fromStdString(gs.memory_card_1_shared_path)); + } + if (!gs.memory_card_2_shared_path.empty()) + { + QSignalBlocker sb(m_ui.userMemoryCard2SharedPath); + m_ui.userMemoryCard2SharedPath->setText(QString::fromStdString(gs.memory_card_2_shared_path)); } } @@ -306,6 +377,24 @@ void GamePropertiesDialog::connectUi() saveGameSettings(); }); + connectBooleanUserSetting(m_ui.userLinearUpscaling, &m_game_settings.display_linear_upscaling); + connectBooleanUserSetting(m_ui.userIntegerUpscaling, &m_game_settings.display_integer_upscaling); + + connect(m_ui.userResolutionScale, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { + if (index <= 0) + m_game_settings.gpu_resolution_scale.reset(); + else + m_game_settings.gpu_resolution_scale = static_cast(index - 1); + saveGameSettings(); + }); + + connectBooleanUserSetting(m_ui.userTrueColor, &m_game_settings.gpu_true_color); + connectBooleanUserSetting(m_ui.userScaledDithering, &m_game_settings.gpu_scaled_dithering); + connectBooleanUserSetting(m_ui.userForceNTSCTimings, &m_game_settings.gpu_force_ntsc_timings); + connectBooleanUserSetting(m_ui.userBilinearTextureFiltering, &m_game_settings.gpu_bilinear_texture_filtering); + connectBooleanUserSetting(m_ui.userWidescreenHack, &m_game_settings.gpu_widescreen_hack); + connectBooleanUserSetting(m_ui.userPGXP, &m_game_settings.gpu_pgxp); + connect(m_ui.userControllerType1, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { if (index <= 0) m_game_settings.controller_1_type.reset(); @@ -322,13 +411,50 @@ void GamePropertiesDialog::connectUi() saveGameSettings(); }); - connect(m_ui.userWidescreenHack, &QCheckBox::stateChanged, [this](int state) { - if (state == Qt::PartiallyChecked) - m_game_settings.gpu_widescreen_hack.reset(); + connect(m_ui.userMemoryCard1Type, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { + if (index <= 0) + m_game_settings.memory_card_1_type.reset(); else - m_game_settings.gpu_widescreen_hack = (state == Qt::Checked); + m_game_settings.memory_card_1_type = static_cast(index - 1); saveGameSettings(); }); + connect(m_ui.userMemoryCard1SharedPath, &QLineEdit::textChanged, [this](const QString& text) { + if (text.isEmpty()) + std::string().swap(m_game_settings.memory_card_1_shared_path); + else + m_game_settings.memory_card_1_shared_path = text.toStdString(); + saveGameSettings(); + }); + connect(m_ui.userMemoryCard1SharedPathBrowse, &QPushButton::clicked, [this]() { + QString path = QFileDialog::getOpenFileName(this, tr("Select path to memory card image"), QString(), + qApp->translate("MemoryCardSettingsWidget", MEMORY_CARD_IMAGE_FILTER)); + if (path.isEmpty()) + return; + + m_ui.userMemoryCard1SharedPath->setText(path); + }); + connect(m_ui.userMemoryCard2Type, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { + if (index <= 0) + m_game_settings.memory_card_2_type.reset(); + else + m_game_settings.memory_card_2_type = static_cast(index - 1); + saveGameSettings(); + }); + connect(m_ui.userMemoryCard2SharedPath, &QLineEdit::textChanged, [this](const QString& text) { + if (text.isEmpty()) + std::string().swap(m_game_settings.memory_card_2_shared_path); + else + m_game_settings.memory_card_2_shared_path = text.toStdString(); + saveGameSettings(); + }); + connect(m_ui.userMemoryCard2SharedPathBrowse, &QPushButton::clicked, [this]() { + QString path = QFileDialog::getOpenFileName(this, tr("Select path to memory card image"), QString(), + qApp->translate("MemoryCardSettingsWidget", MEMORY_CARD_IMAGE_FILTER)); + if (path.isEmpty()) + return; + + m_ui.userMemoryCard2SharedPath->setText(path); + }); for (u32 i = 0; i < static_cast(GameSettings::Trait::Count); i++) { diff --git a/src/duckstation-qt/gamepropertiesdialog.h b/src/duckstation-qt/gamepropertiesdialog.h index 65f74c22e..ed2e74aab 100644 --- a/src/duckstation-qt/gamepropertiesdialog.h +++ b/src/duckstation-qt/gamepropertiesdialog.h @@ -43,6 +43,8 @@ private: void populateCompatibilityInfo(const std::string& game_code); void populateTracksInfo(const std::string& image_path); void populateGameSettings(); + void populateBooleanUserSetting(QCheckBox* cb, const std::optional& value); + void connectBooleanUserSetting(QCheckBox* cb, std::optional* value); void saveGameSettings(); void fillEntryFromUi(GameListCompatibilityEntry* entry); void computeTrackHashes(); diff --git a/src/duckstation-qt/gamepropertiesdialog.ui b/src/duckstation-qt/gamepropertiesdialog.ui index e6a800ede..e6af3e8bf 100644 --- a/src/duckstation-qt/gamepropertiesdialog.ui +++ b/src/duckstation-qt/gamepropertiesdialog.ui @@ -7,7 +7,7 @@ 0 0 793 - 647 + 600 @@ -21,7 +21,7 @@ - 0 + 1 @@ -190,39 +190,132 @@ - GPU Settings + GPU Screen Display - - - Crop Mode: - - - - - - - Aspect Ratio: - + - - + + - Widescreen Hack + Crop Mode: + + + + + + + + + + Linear Upscaling true + + + + Integer Upscaling + + + true + + + + + + + + + + GPU Enhancements + + + + + + Resolution Scale: + + + + + + + + + + + + True Color Rendering (24-bit, disables dithering) + + + true + + + + + + + Scaled Dithering (scale dither pattern to resolution) + + + true + + + + + + + Widescreen Hack + + + true + + + + + + + Force NTSC Timings (60hz-on-PAL) + + + true + + + + + + + Bilinear Texture Filtering + + + true + + + + + + + PGXP Geometry Correction + + + true + + + + + @@ -256,7 +349,78 @@ - + + + Memory Card Settings + + + + + + Memory Card 1 Type: + + + + + + + + + + Memory Card 1 Shared Path: + + + + + + + + + + + + Browse... + + + + + + + + + Memory Card 2 Type: + + + + + + + + + + Memory Card 2 Shared Path: + + + + + + + + + + + + Browse... + + + + + + + + + + Qt::Vertical diff --git a/src/duckstation-qt/gpusettingswidget.cpp b/src/duckstation-qt/gpusettingswidget.cpp index 2f2f2919f..15964ad4f 100644 --- a/src/duckstation-qt/gpusettingswidget.cpp +++ b/src/duckstation-qt/gpusettingswidget.cpp @@ -1,6 +1,7 @@ #include "gpusettingswidget.h" #include "core/gpu.h" #include "core/settings.h" +#include "qtutils.h" #include "settingsdialog.h" #include "settingwidgetbinder.h" @@ -62,14 +63,15 @@ GPUSettingsWidget::GPUSettingsWidget(QtHostInterface* host_interface, QWidget* p dialog->registerWidgetHelp( m_ui.renderer, tr("Renderer"), Settings::GetRendererDisplayName(Settings::DEFAULT_GPU_RENDERER), - tr( - "Chooses the backend to use for rendering the console/game visuals.
Depending on your system and hardware, " - "Direct3D 11 and OpenGL hardware backends may be available.
The software renderer offers the best compatibility, " - "but is the slowest and does not offer any enhancements.")); + tr("Chooses the backend to use for rendering the console/game visuals.
Depending on your system and hardware, " + "Direct3D 11 and OpenGL hardware backends may be available.
The software renderer offers the best " + "compatibility, " + "but is the slowest and does not offer any enhancements.")); dialog->registerWidgetHelp( m_ui.adapter, tr("Adapter"), tr("(Default)"), tr("If your system contains multiple GPUs or adapters, you can select which GPU you wish to use for the hardware " - "renderers.
This option is only supported in Direct3D and Vulkan. OpenGL will always use the default device.")); + "renderers.
This option is only supported in Direct3D and Vulkan. OpenGL will always use the default " + "device.")); dialog->registerWidgetHelp( m_ui.displayAspectRatio, tr("Aspect Ratio"), QStringLiteral("4:3"), tr("Changes the aspect ratio used to display the console's output to the screen. The default " @@ -82,14 +84,16 @@ GPUSettingsWidget::GPUSettingsWidget(QtHostInterface* host_interface, QWidget* p "compromise between stability and hiding black borders.")); dialog->registerWidgetHelp( m_ui.disableInterlacing, tr("Disable Interlacing (force progressive render/scan)"), tr("Unchecked"), - tr("Forces the rendering and display of frames to progressive mode.
This removes the \"combing\" effect seen in " - "480i games by rendering them in 480p. Usually safe to enable.
" - "May not be compatible with all games.")); - dialog->registerWidgetHelp( - m_ui.displayLinearFiltering, tr("Linear Upscaling"), tr("Checked"), - tr("Uses bilinear texture filtering when displaying the console's framebuffer to the screen.
Disabling filtering " - "will producer a sharper, blockier/pixelated image. Enabling will smooth out the image.
The option will be less " - "noticable the higher the resolution scale.")); + tr( + "Forces the rendering and display of frames to progressive mode.
This removes the \"combing\" effect seen in " + "480i games by rendering them in 480p. Usually safe to enable.
" + "May not be compatible with all games.")); + dialog->registerWidgetHelp(m_ui.displayLinearFiltering, tr("Linear Upscaling"), tr("Checked"), + tr("Uses bilinear texture filtering when displaying the console's framebuffer to the " + "screen.
Disabling filtering " + "will producer a sharper, blockier/pixelated image. Enabling will smooth out the " + "image.
The option will be less " + "noticable the higher the resolution scale.")); dialog->registerWidgetHelp( m_ui.displayIntegerScaling, tr("Integer Upscaling"), tr("Unchecked"), tr("Adds padding to the display area to ensure that the ratio between pixels on the host to " @@ -114,12 +118,12 @@ GPUSettingsWidget::GPUSettingsWidget(QtHostInterface* host_interface, QWidget* p m_ui.scaledDithering, tr("Scaled Dithering (scale dither pattern to resolution)"), tr("Checked"), tr("Scales the dither pattern to the resolution scale of the emulated GPU. This makes the dither pattern much less " "obvious at higher resolutions.
Usually safe to enable, and only supported by the hardware renderers.")); - dialog->registerWidgetHelp( - m_ui.forceNTSCTimings, tr("Force NTSC Timings (60hz-on-PAL)"), tr("Unchecked"), - tr( - "Uses NTSC frame timings when the console is in PAL mode, forcing PAL games to run at 60hz.
For most games which " - "have a speed tied to the framerate, this will result in the game running approximately 17% faster.
For variable " - "frame rate games, it may not affect the speed.")); + dialog->registerWidgetHelp(m_ui.forceNTSCTimings, tr("Force NTSC Timings (60hz-on-PAL)"), tr("Unchecked"), + tr("Uses NTSC frame timings when the console is in PAL mode, forcing PAL games to run at " + "60hz.
For most games which " + "have a speed tied to the framerate, this will result in the game running " + "approximately 17% faster.
For variable " + "frame rate games, it may not affect the speed.")); dialog->registerWidgetHelp( m_ui.linearTextureFiltering, tr("Bilinear Texture Filtering"), tr("Unchecked"), tr("Smooths out the blockyness of magnified textures on 3D object by using bilinear filtering.
Will have a " @@ -128,7 +132,8 @@ GPUSettingsWidget::GPUSettingsWidget(QtHostInterface* host_interface, QWidget* p m_ui.widescreenHack, tr("Widescreen Hack"), tr("Unchecked"), tr("Scales vertex positions in screen-space to a widescreen aspect ratio, essentially " "increasing the field of view from 4:3 to 16:9 in 3D games.
For 2D games, or games which " - "use pre-rendered backgrounds, this enhancement will not work as expected.
May not be compatible with all games.")); + "use pre-rendered backgrounds, this enhancement will not work as expected.
May not be compatible with " + "all games.")); dialog->registerWidgetHelp( m_ui.pgxpEnable, tr("Geometry Correction"), tr("Unchecked"), tr("Reduces \"wobbly\" polygons and \"warping\" textures that are common in PS1 games.
Only " @@ -178,29 +183,7 @@ void GPUSettingsWidget::setupAdditionalUi() qApp->translate("DisplayCropMode", Settings::GetDisplayCropModeDisplayName(static_cast(i)))); } - std::array resolution_suffixes = {{ - QString(), // auto - QString(), // 1x - QString(), // 2x - tr(" (for 720p)"), // 3x - QString(), // 4x - tr(" (for 1080p)"), // 5x - tr(" (for 1440p)"), // 6x - QString(), // 7x - QString(), // 8x - tr(" (for 4K)"), // 9x - QString(), // 10x - QString(), // 11x - QString(), // 12x - QString(), // 13x - QString(), // 14x - QString(), // 15x - QString() // 16x - }}; - - m_ui.resolutionScale->addItem(tr("Automatic based on window size")); - for (u32 i = 1; i <= GPU::MAX_RESOLUTION_SCALE; i++) - m_ui.resolutionScale->addItem(tr("%1x%2").arg(i).arg(resolution_suffixes[i])); + QtUtils::FillComboBoxWithResolutionScales(m_ui.resolutionScale); } void GPUSettingsWidget::populateGPUAdapters() diff --git a/src/duckstation-qt/qtutils.cpp b/src/duckstation-qt/qtutils.cpp index 9df725acb..1122cdf00 100644 --- a/src/duckstation-qt/qtutils.cpp +++ b/src/duckstation-qt/qtutils.cpp @@ -1,8 +1,10 @@ #include "qtutils.h" #include "common/byte_stream.h" +#include #include #include #include +#include #include #include #include @@ -646,4 +648,25 @@ void OpenURL(QWidget* parent, const char* url) return OpenURL(parent, QUrl::fromEncoded(QByteArray(url, static_cast(std::strlen(url))))); } +void FillComboBoxWithResolutionScales(QComboBox* cb) +{ + cb->addItem(qApp->translate("GPUSettingsWidget", "Automatic based on window size")); + cb->addItem(qApp->translate("GPUSettingsWidget", "1x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "2x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "3x (for 720p)")); + cb->addItem(qApp->translate("GPUSettingsWidget", "4x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "5x (for 1080p)")); + cb->addItem(qApp->translate("GPUSettingsWidget", "6x (for 1440p)")); + cb->addItem(qApp->translate("GPUSettingsWidget", "7x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "8x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "9x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "10x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "11x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "12x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "13x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "14x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "15x")); + cb->addItem(qApp->translate("GPUSettingsWidget", "16x")); +} + } // namespace QtUtils \ No newline at end of file diff --git a/src/duckstation-qt/qtutils.h b/src/duckstation-qt/qtutils.h index 5eb42093a..b0df67fe9 100644 --- a/src/duckstation-qt/qtutils.h +++ b/src/duckstation-qt/qtutils.h @@ -9,6 +9,7 @@ Q_DECLARE_METATYPE(std::optional); class ByteStream; +class QComboBox; class QFrame; class QKeyEvent; class QTableView; @@ -54,4 +55,7 @@ void OpenURL(QWidget* parent, const QUrl& qurl); /// Opens a URL string with the default handler. void OpenURL(QWidget* parent, const char* url); +/// Fills a combo box with resolution scale options. +void FillComboBoxWithResolutionScales(QComboBox* cb); + } // namespace QtUtils \ No newline at end of file diff --git a/src/frontend-common/game_list.h b/src/frontend-common/game_list.h index aac50561d..7b00c602a 100644 --- a/src/frontend-common/game_list.h +++ b/src/frontend-common/game_list.h @@ -112,7 +112,7 @@ private: enum : u32 { GAME_LIST_CACHE_SIGNATURE = 0x45434C47, - GAME_LIST_CACHE_VERSION = 7 + GAME_LIST_CACHE_VERSION = 8 }; using DatabaseMap = std::unordered_map; diff --git a/src/frontend-common/game_settings.cpp b/src/frontend-common/game_settings.cpp index 7b2bc84af..9b9cf2228 100644 --- a/src/frontend-common/game_settings.cpp +++ b/src/frontend-common/game_settings.cpp @@ -70,6 +70,19 @@ bool ReadOptionalFromStream(ByteStream* stream, std::optional* dest) return true; } +static bool ReadStringFromStream(ByteStream* stream, std::string* dest) +{ + u32 size; + if (!stream->Read2(&size, sizeof(size))) + return false; + + dest->resize(size); + if (!stream->Read2(dest->data(), size)) + return false; + + return true; +} + template bool WriteOptionalToStream(ByteStream* stream, const std::optional& src) { @@ -83,6 +96,12 @@ bool WriteOptionalToStream(ByteStream* stream, const std::optional& src) return stream->Write2(&src.value(), sizeof(T)); } +static bool WriteStringToStream(ByteStream* stream, const std::string& str) +{ + const u32 size = static_cast(str.size()); + return (stream->Write2(&size, sizeof(size)) && (size == 0 || stream->Write2(str.data(), size))); +} + bool Entry::LoadFromStream(ByteStream* stream) { constexpr u32 num_bytes = (static_cast(Trait::Count) + 7) / 8; @@ -91,8 +110,17 @@ bool Entry::LoadFromStream(ByteStream* stream) if (!stream->Read2(bits.data(), num_bytes) || !ReadOptionalFromStream(stream, &display_active_start_offset) || !ReadOptionalFromStream(stream, &display_active_end_offset) || !ReadOptionalFromStream(stream, &display_crop_mode) || !ReadOptionalFromStream(stream, &display_aspect_ratio) || + !ReadOptionalFromStream(stream, &display_linear_upscaling) || + !ReadOptionalFromStream(stream, &display_integer_upscaling) || + !ReadOptionalFromStream(stream, &gpu_resolution_scale) || !ReadOptionalFromStream(stream, &gpu_true_color) || + !ReadOptionalFromStream(stream, &gpu_scaled_dithering) || + !ReadOptionalFromStream(stream, &gpu_force_ntsc_timings) || + !ReadOptionalFromStream(stream, &gpu_bilinear_texture_filtering) || + !ReadOptionalFromStream(stream, &gpu_widescreen_hack) || !ReadOptionalFromStream(stream, &gpu_pgxp) || !ReadOptionalFromStream(stream, &controller_1_type) || !ReadOptionalFromStream(stream, &controller_2_type) || - !ReadOptionalFromStream(stream, &gpu_widescreen_hack)) + !ReadOptionalFromStream(stream, &memory_card_1_type) || !ReadOptionalFromStream(stream, &memory_card_2_type) || + !ReadStringFromStream(stream, &memory_card_1_shared_path) || + !ReadStringFromStream(stream, &memory_card_2_shared_path)) { return false; } @@ -120,8 +148,17 @@ bool Entry::SaveToStream(ByteStream* stream) const return stream->Write2(bits.data(), num_bytes) && WriteOptionalToStream(stream, display_active_start_offset) && WriteOptionalToStream(stream, display_active_end_offset) && WriteOptionalToStream(stream, display_crop_mode) && - WriteOptionalToStream(stream, display_aspect_ratio) && WriteOptionalToStream(stream, controller_1_type) && - WriteOptionalToStream(stream, controller_2_type) && WriteOptionalToStream(stream, gpu_widescreen_hack); + WriteOptionalToStream(stream, display_linear_upscaling) && + WriteOptionalToStream(stream, display_integer_upscaling) && + WriteOptionalToStream(stream, display_aspect_ratio) && WriteOptionalToStream(stream, gpu_resolution_scale) && + WriteOptionalToStream(stream, gpu_true_color) && WriteOptionalToStream(stream, gpu_scaled_dithering) && + WriteOptionalToStream(stream, gpu_force_ntsc_timings) && + WriteOptionalToStream(stream, gpu_bilinear_texture_filtering) && + WriteOptionalToStream(stream, gpu_widescreen_hack) && WriteOptionalToStream(stream, gpu_pgxp) && + WriteOptionalToStream(stream, controller_1_type) && WriteOptionalToStream(stream, controller_2_type) && + WriteOptionalToStream(stream, memory_card_1_type) && WriteOptionalToStream(stream, memory_card_2_type) && + WriteStringToStream(stream, memory_card_1_shared_path) && + WriteStringToStream(stream, memory_card_2_shared_path); } static void ParseIniSection(Entry* entry, const char* section, const CSimpleIniA& ini) @@ -145,6 +182,34 @@ static void ParseIniSection(Entry* entry, const char* section, const CSimpleIniA cvalue = ini.GetValue(section, "DisplayAspectRatio", nullptr); if (cvalue) entry->display_aspect_ratio = Settings::ParseDisplayAspectRatio(cvalue); + cvalue = ini.GetValue(section, "DisplayLinearUpscaling", nullptr); + if (cvalue) + entry->display_linear_upscaling = StringUtil::FromChars(cvalue); + cvalue = ini.GetValue(section, "DisplayIntegerUpscaling", nullptr); + if (cvalue) + entry->display_integer_upscaling = StringUtil::FromChars(cvalue); + + cvalue = ini.GetValue(section, "GPUResolutionScale", nullptr); + if (cvalue) + entry->gpu_resolution_scale = StringUtil::FromChars(cvalue); + cvalue = ini.GetValue(section, "GPUTrueColor", nullptr); + if (cvalue) + entry->gpu_true_color = StringUtil::FromChars(cvalue); + cvalue = ini.GetValue(section, "GPUScaledDithering", nullptr); + if (cvalue) + entry->gpu_scaled_dithering = StringUtil::FromChars(cvalue); + cvalue = ini.GetValue(section, "GPUBilinearTextureFiltering", nullptr); + if (cvalue) + entry->gpu_bilinear_texture_filtering = StringUtil::FromChars(cvalue); + cvalue = ini.GetValue(section, "GPUForceNTSCTimings", nullptr); + if (cvalue) + entry->gpu_force_ntsc_timings = StringUtil::FromChars(cvalue); + cvalue = ini.GetValue(section, "GPUWidescreenHack", nullptr); + if (cvalue) + entry->gpu_widescreen_hack = StringUtil::FromChars(cvalue); + cvalue = ini.GetValue(section, "GPUPGXP", nullptr); + if (cvalue) + entry->gpu_pgxp = StringUtil::FromChars(cvalue); cvalue = ini.GetValue(section, "Controller1Type", nullptr); if (cvalue) @@ -153,9 +218,18 @@ static void ParseIniSection(Entry* entry, const char* section, const CSimpleIniA if (cvalue) entry->controller_2_type = Settings::ParseControllerTypeName(cvalue); - cvalue = ini.GetValue(section, "GPUWidescreenHack", nullptr); + cvalue = ini.GetValue(section, "MemoryCard1Type", nullptr); if (cvalue) - entry->gpu_widescreen_hack = StringUtil::FromChars(cvalue); + entry->memory_card_1_type = Settings::ParseMemoryCardTypeName(cvalue); + cvalue = ini.GetValue(section, "MemoryCard2Type", nullptr); + if (cvalue) + entry->memory_card_2_type = Settings::ParseMemoryCardTypeName(cvalue); + cvalue = ini.GetValue(section, "MemoryCard1SharedPath"); + if (cvalue) + entry->memory_card_1_shared_path = cvalue; + cvalue = ini.GetValue(section, "MemoryCard2SharedPath"); + if (cvalue) + entry->memory_card_2_shared_path = cvalue; } static void StoreIniSection(const Entry& entry, const char* section, CSimpleIniA& ini) @@ -179,14 +253,46 @@ static void StoreIniSection(const Entry& entry, const char* section, CSimpleIniA ini.SetValue(section, "DisplayAspectRatio", Settings::GetDisplayAspectRatioName(entry.display_aspect_ratio.value())); } + if (entry.display_linear_upscaling.has_value()) + ini.SetValue(section, "DisplayLinearUpscaling", entry.display_linear_upscaling.value() ? "true" : "false"); + if (entry.display_integer_upscaling.has_value()) + ini.SetValue(section, "DisplayIntegerUpscaling", entry.display_integer_upscaling.value() ? "true" : "false"); + + if (entry.gpu_resolution_scale.has_value()) + ini.SetLongValue(section, "GPUResolutionScale", static_cast(entry.gpu_resolution_scale.value())); + if (entry.gpu_true_color.has_value()) + ini.SetValue(section, "GPUTrueColor", entry.gpu_true_color.value() ? "true" : "false"); + if (entry.gpu_scaled_dithering.has_value()) + ini.SetValue(section, "GPUScaledDithering", entry.gpu_scaled_dithering.value() ? "true" : "false"); + if (entry.gpu_bilinear_texture_filtering.has_value()) + { + ini.SetValue(section, "GPUBilinearTextureFiltering", + entry.gpu_bilinear_texture_filtering.value() ? "true" : "false"); + } + if (entry.gpu_force_ntsc_timings.has_value()) + ini.SetValue(section, "GPUForceNTSCTimings", entry.gpu_force_ntsc_timings.value() ? "true" : "false"); + if (entry.gpu_widescreen_hack.has_value()) + ini.SetValue(section, "GPUWidescreenHack", entry.gpu_widescreen_hack.value() ? "true" : "false"); + if (entry.gpu_pgxp.has_value()) + ini.SetValue(section, "GPUPGXP", entry.gpu_pgxp.value() ? "true" : "false"); if (entry.controller_1_type.has_value()) ini.SetValue(section, "Controller1Type", Settings::GetControllerTypeName(entry.controller_1_type.value())); if (entry.controller_2_type.has_value()) ini.SetValue(section, "Controller2Type", Settings::GetControllerTypeName(entry.controller_2_type.value())); - if (entry.gpu_widescreen_hack.has_value()) - ini.SetValue(section, "GPUWidescreenHack", entry.gpu_widescreen_hack.value() ? "true" : "false"); + if (entry.controller_1_type.has_value()) + ini.SetValue(section, "Controller1Type", Settings::GetControllerTypeName(entry.controller_1_type.value())); + if (entry.controller_2_type.has_value()) + ini.SetValue(section, "Controller2Type", Settings::GetControllerTypeName(entry.controller_2_type.value())); + if (entry.memory_card_1_type.has_value()) + ini.SetValue(section, "MemoryCard1Type", Settings::GetMemoryCardTypeName(entry.memory_card_1_type.value())); + if (entry.memory_card_2_type.has_value()) + ini.SetValue(section, "MemoryCard2Type", Settings::GetMemoryCardTypeName(entry.memory_card_2_type.value())); + if (!entry.memory_card_1_shared_path.empty()) + ini.SetValue(section, "MemoryCard1SharedPath", entry.memory_card_1_shared_path.c_str()); + if (!entry.memory_card_2_shared_path.empty()) + ini.SetValue(section, "MemoryCard2SharedPath", entry.memory_card_2_shared_path.c_str()); } Database::Database() = default; @@ -297,12 +403,39 @@ void Entry::ApplySettings(bool display_osd_messages) const g_settings.display_crop_mode = display_crop_mode.value(); if (display_aspect_ratio.has_value()) g_settings.display_aspect_ratio = display_aspect_ratio.value(); + if (display_linear_upscaling.has_value()) + g_settings.display_linear_filtering = display_linear_upscaling.value(); + if (display_integer_upscaling.has_value()) + g_settings.display_integer_scaling = display_integer_upscaling.value(); + + if (gpu_resolution_scale.has_value()) + g_settings.gpu_resolution_scale = gpu_resolution_scale.value(); + if (gpu_true_color.has_value()) + g_settings.gpu_true_color = gpu_true_color.value(); + if (gpu_scaled_dithering.has_value()) + g_settings.gpu_scaled_dithering = gpu_scaled_dithering.value(); + if (gpu_force_ntsc_timings.has_value()) + g_settings.gpu_force_ntsc_timings = gpu_force_ntsc_timings.value(); + if (gpu_bilinear_texture_filtering) + g_settings.gpu_texture_filtering = gpu_bilinear_texture_filtering.value(); + if (gpu_widescreen_hack.has_value()) + g_settings.gpu_widescreen_hack = gpu_widescreen_hack.value(); + if (gpu_pgxp.has_value()) + g_settings.gpu_pgxp_enable = gpu_pgxp.value(); + if (controller_1_type.has_value()) g_settings.controller_types[0] = controller_1_type.value(); if (controller_2_type.has_value()) g_settings.controller_types[1] = controller_2_type.value(); - if (gpu_widescreen_hack.has_value()) - g_settings.gpu_widescreen_hack = gpu_widescreen_hack.value(); + + if (memory_card_1_type.has_value()) + g_settings.memory_card_types[0] = memory_card_1_type.value(); + if (!memory_card_1_shared_path.empty()) + g_settings.memory_card_paths[0] = memory_card_1_shared_path; + if (memory_card_2_type.has_value()) + g_settings.memory_card_types[1] = memory_card_2_type.value(); + if (!memory_card_1_shared_path.empty()) + g_settings.memory_card_paths[1] = memory_card_2_shared_path; if (HasTrait(Trait::ForceInterpreter)) { diff --git a/src/frontend-common/game_settings.h b/src/frontend-common/game_settings.h index 918210ab1..176bea0b3 100644 --- a/src/frontend-common/game_settings.h +++ b/src/frontend-common/game_settings.h @@ -40,9 +40,21 @@ struct Entry // user settings std::optional display_crop_mode; std::optional display_aspect_ratio; + std::optional display_linear_upscaling; + std::optional display_integer_upscaling; + std::optional gpu_resolution_scale; + std::optional gpu_true_color; + std::optional gpu_scaled_dithering; + std::optional gpu_force_ntsc_timings; + std::optional gpu_bilinear_texture_filtering; + std::optional gpu_widescreen_hack; + std::optional gpu_pgxp; std::optional controller_1_type; std::optional controller_2_type; - std::optional gpu_widescreen_hack; + std::optional memory_card_1_type; + std::optional memory_card_2_type; + std::string memory_card_1_shared_path; + std::string memory_card_2_shared_path; ALWAYS_INLINE bool HasTrait(Trait trait) const { return traits[static_cast(trait)]; } ALWAYS_INLINE void AddTrait(Trait trait) { traits[static_cast(trait)] = true; } From b2057ac6cc0035a05874c79553f5499af22afc04 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Wed, 2 Sep 2020 00:03:53 +1000 Subject: [PATCH 46/61] GameSettings: Add disallow force NTSC timings trait --- src/frontend-common/game_settings.cpp | 13 +++++++++++++ src/frontend-common/game_settings.h | 1 + 2 files changed, 14 insertions(+) diff --git a/src/frontend-common/game_settings.cpp b/src/frontend-common/game_settings.cpp index 9b9cf2228..4e655de20 100644 --- a/src/frontend-common/game_settings.cpp +++ b/src/frontend-common/game_settings.cpp @@ -25,6 +25,7 @@ std::array, static_cast(Trait::Count)> {"DisableTrueColor", TRANSLATABLE("GameSettingsTrait", "Disable True Color")}, {"DisableUpscaling", TRANSLATABLE("GameSettingsTrait", "Disable Upscaling")}, {"DisableScaledDithering", TRANSLATABLE("GameSettingsTrait", "Disable Scaled Dithering")}, + {"DisableForceNTSCTimings", TRANSLATABLE("GameSettingsTrait", "Disallow Forcing NTSC Timings")}, {"DisableWidescreen", TRANSLATABLE("GameSettingsTrait", "Disable Widescreen")}, {"DisablePGXP", TRANSLATABLE("GameSettingsTrait", "Disable PGXP")}, {"DisablePGXPCulling", TRANSLATABLE("GameSettingsTrait", "Disable PGXP Culling")}, @@ -517,6 +518,18 @@ void Entry::ApplySettings(bool display_osd_messages) const g_settings.gpu_widescreen_hack = false; } + if (HasTrait(Trait::DisableForceNTSCTimings)) + { + if (display_osd_messages && g_settings.gpu_force_ntsc_timings) + { + g_host_interface->AddOSDMessage( + g_host_interface->TranslateStdString("OSDMessage", "Forcing NTSC Timings disallowed by game settings."), + osd_duration); + } + + g_settings.gpu_force_ntsc_timings = false; + } + if (HasTrait(Trait::DisablePGXP)) { if (display_osd_messages && g_settings.gpu_pgxp_enable) diff --git a/src/frontend-common/game_settings.h b/src/frontend-common/game_settings.h index 176bea0b3..3f67eb08c 100644 --- a/src/frontend-common/game_settings.h +++ b/src/frontend-common/game_settings.h @@ -16,6 +16,7 @@ enum class Trait : u32 DisableTrueColor, DisableUpscaling, DisableScaledDithering, + DisableForceNTSCTimings, DisableWidescreen, DisablePGXP, DisablePGXPCulling, From 355b1627587d5ecc1da944da999a79d48e758e70 Mon Sep 17 00:00:00 2001 From: Anderson_Cardoso <43047877+andercard0@users.noreply.github.com> Date: Tue, 1 Sep 2020 21:28:05 -0300 Subject: [PATCH 47/61] Quick update - Pt-Br and Workaround MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translation Notes: - Quick workaround fix for some ~, é, ç accentuations not properly being show; - Translation of other bunch more user settings added recently; - Same Tabs still not translatable as before; --- .../translations/duckstation-qt_pt-br.ts | 756 ++++++++++++------ 1 file changed, 517 insertions(+), 239 deletions(-) diff --git a/src/duckstation-qt/translations/duckstation-qt_pt-br.ts b/src/duckstation-qt/translations/duckstation-qt_pt-br.ts index 9bf0b0c64..fb62dc8f0 100644 --- a/src/duckstation-qt/translations/duckstation-qt_pt-br.ts +++ b/src/duckstation-qt/translations/duckstation-qt_pt-br.ts @@ -112,41 +112,53 @@ GPU Max Run-Ahead: - + + + Enable Recompiler ICache + Ativar Recompilador ICache + + + Reset To Default Redefinir para o Padrão - + Enable Recompiler Memory Exceptions Habilitar Exceções de Memória - + System Settings Configurações do Sistema - + Show Debug Menu Mostrar Menu de Depuração - - + + Use Debug Host GPU Device Usar GPU para depuração - + + Unchecked Desmarcado - + Enables the usage of debug devices and shaders for rendering APIs which support them. Should only be used when debugging the emulator. Permite o uso de dispositivos de depuração e shaders para renderizar APIs que os suportam. Só deve ser usado ao depurar o emulador. + + + Determines whether the CPU's instruction cache is simulated in the recompiler. Improves accuracy at a small cost to performance. If games are running too fast, try enabling this option. + Determina se a instrução enviada ao CPU emulado fica armazenada no recompilador. Melhor a precisão ao pequeno custo de performance. Se os jogos estão rodando muito rápido, tente ativar esta opção. + AnalogController @@ -305,17 +317,17 @@ AudioBackend - + Null (No Output) Nulo (Sem som) - + Cubeb Cubed - + SDL @@ -433,7 +445,7 @@ Controls the volume of the audio played on the host. Values are in percentage. - Controla o volume do aúdio. Valores são mostrados em porcentagem. + Controla o volume do áudio. Valores são mostrados em porcentagem. @@ -534,17 +546,17 @@ CPUExecutionMode - + Intepreter (Slowest) Interpretador (Mais Lento) - + Cached Interpreter (Faster) Int. Armazenado (Rápido) - + Recompiler (Fastest) Recompilador (Mais Rápido) @@ -552,12 +564,12 @@ CommonHostInterface - + Are you sure you want to stop emulation? - Quer mesmo parar a Emulação? + Quer mesmo parar a Emulacao? - + The current state will be saved. O estado atual será salvo. @@ -565,22 +577,22 @@ ConsoleRegion - + Auto-Detect Auto Detectar - + NTSC-J (Japan) NTSC-J (Japão) - + NTSC-U (US) NTSC-U (US) - + PAL (Europe, Australia) PAL (Europeu, Australia) @@ -814,32 +826,32 @@ ControllerType - + None Nenhum - + Digital Controller Controle Digital - + Analog Controller (DualShock) Controle Analogico (Dualshock) - + Namco GunCon Namco GunCon - + PlayStation Mouse Playstation Mouse - + NeGcon NeGcon @@ -928,22 +940,22 @@ DiscRegion - + NTSC-J (Japan) NTSC-J (Japão) - + NTSC-U (US) NTSC-U (US) - + PAL (Europe, Australia) PAL (Europeu, Australia) - + Other Outros @@ -951,17 +963,17 @@ DisplayCropMode - + None Nenhum - + Only Overscan Area Somente Área Renderizada - + All Borders Todas as Bordas @@ -969,22 +981,22 @@ GPURenderer - + Hardware (D3D11) Placa de Video (D3D11) - + Hardware (Vulkan) Placa de Video (Vulkan) - + Hardware (OpenGL) Placa de Video (OpenGL) - + Software Software @@ -994,7 +1006,7 @@ Form - + Form @@ -1032,19 +1044,19 @@ - + Linear Upscaling Escalonamento Linear - + Integer Upscaling Escalonamento Integro - + VSync Sincronização Vertical (V-Sync) @@ -1060,37 +1072,37 @@ - + True Color Rendering (24-bit, disables dithering) Renderização em (24 Cores, desativa o efeito dithering) - + Scaled Dithering (scale dither pattern to resolution) Dithering Escalonado, (Escalona o padrão do dithering para a resolução) - + Disable Interlacing (force progressive render/scan) Desativa o entrelaçamento (Força Rederização Progressiva) - + Force NTSC Timings (60hz-on-PAL) Força o temporizador rodar em NTSC (60hz em jogos EU) - + Bilinear Texture Filtering Filtragem de Textura Bilinear - + Widescreen Hack Hack para Telas Widescreen @@ -1101,36 +1113,36 @@ - + Geometry Correction Correção Geométrica - + Culling Correction Correção de Curvas - + Texture Correction Correção de Textura - + Vertex Cache Vertice Armazenado - + CPU Mode Modo CPU - + Renderer Rederizador @@ -1139,7 +1151,7 @@ Escolhe a opção a ser usada para emular a GPU. Dependendo do seu sistema e hardware, As opções DX11 e OpenGL podem aparecer.O renderizador de software oferece a melhor compatibilidade, mas é o mais lento e não oferece nenhum aprimoramento. - + Adapter Adaptador @@ -1148,15 +1160,15 @@ Se você tem várias GPUs ,você poderá selecionar qual delas deseja usar para os renderizadores de hardware. Esta opção é suportada apenas no Direct3D e no Vulkan, OpenGL sempre usará o dispositivo padrão. - - - - - + + + + - - - + + + + Unchecked Desmarcado @@ -1165,22 +1177,22 @@ Permite o uso de dispositivos de depuração e shaders para renderizar APIs que os suportam. Só deve ser usado ao depurar o emulador. - + Aspect Ratio Razão de Aspecto - + Changes the aspect ratio used to display the console's output to the screen. The default is 4:3 which matches a typical TV of the era. Altera a proporção usada para exibir o jogo na tela. O padrão é 4:3, que corresponde a uma TV típica da época.CRT mais conhecida como Tubão. - + Crop Mode Modo de Corte - + Only Overscan Area Somente Área Renderizada @@ -1201,11 +1213,11 @@ Força o modo de quadros por segundo em modo progressivo. Se o jogo já tem essa opção nativamente ele não irá ter nenhum beneficio podendo assim deixar a mesma ligada. - - - - - + + + + + Checked Marcado @@ -1222,7 +1234,7 @@ Ativa a sincronização quando possível. A ativação dessa opção fornecerá melhor ritmo de quadros por segundo e movimento mais suave com menos quadros duplicados.<br><br>O V-Sync é desativado automaticamente quando não é possível usá-lo (por exemplo quando o jogo não estiver rodando a 100%). - + Resolution Scale Escala de Resolução @@ -1231,7 +1243,7 @@ Permite o aumento de escala de objetos 3D renderizados, aplica-se apenas aos back-end de hardware é seguro usar essa opção na maioria dos jogos ficando melhor ainda em resoluções mais altas; Isto implica também no maior uso da sua Placa de Video. - + Forces the precision of colours output to the console's framebuffer to use the full 8 bits of precision per channel. This produces nicer looking gradients at the cost of making some colours look slightly different. Disabling the option also enables dithering, which makes the transition between colours less sharp by applying a pattern around those pixels. Most games are compatible with this option, but there is a number which aren't and will have broken effects with it enabled. Only applies to the hardware renderers. Força a precisão das cores produz efeitos de gradientes mais agradável ao custo de fazer com que algumas cores pareçam um pouco diferentes. Desativar a opção também ativa alguns pontilhados, o que torna a transição entre cores menos nítida a maioria dos jogos é compatível com esta opção, os que não forem terão efeitos quebrados com a opção ativada. Aplica-se apenas aos renderizadores por hardware. @@ -1264,127 +1276,202 @@ Escala as posições de vértices para uma proporção de aspecto esticado, aumentando o campo de visão de 4:3 para 16:9 em jogos 3D. <br>Para jogos 2D, ou jogos que usam fundos pré-rederizados, este aprimoramento não funcionará como esperado. <b><u>Pode não ser compatível com todos os jogos</u></b> - + Chooses the backend to use for rendering the console/game visuals. <br>Depending on your system and hardware, Direct3D 11 and OpenGL hardware backends may be available. <br>The software renderer offers the best compatibility, but is the slowest and does not offer any enhancements. Escolhe a opção a ser usada para emular a GPU. Dependendo do seu sistema e hardware, As opções DX11 e OpenGL podem aparecer.O renderizador de software oferece a melhor compatibilidade, mas é o mais lento e não oferece nenhum aprimoramento. - + If your system contains multiple GPUs or adapters, you can select which GPU you wish to use for the hardware renderers. <br>This option is only supported in Direct3D and Vulkan. OpenGL will always use the default device. Se você tem várias GPUs ,você poderá selecionar qual delas deseja usar para os renderizadores de hardware. Esta opção é suportada apenas no Direct3D e no Vulkan, OpenGL sempre usará o dispositivo padrão. - + Determines how much of the area typically not visible on a consumer TV set to crop/hide. <br>Some games display content in the overscan area, or use it for screen effects. <br>May not display correctly with the "All Borders" setting. "Only Overscan" offers a good compromise between stability and hiding black borders. Determina quanto da area normalmente não visivel em uma TV o usuário pode ver ou não.Alguns jogos mostram conteúdo fora desta area pré-determinada.Somente esta opção "overscan" (fora da área visivel) pode oferecer um boa estabilidade na hora de ocultar as tarjas (bordas)pretas quando ocorrem. - + Forces the rendering and display of frames to progressive mode. <br>This removes the "combing" effect seen in 480i games by rendering them in 480p. Usually safe to enable.<br> <b><u>May not be compatible with all games.</u></b> Força a renderização e a exibição de quadros para o modo progressivo. Isso remove efeitos de "trepidação" Visto nos jogos 480i renderizando-os em 480p.Nem todos os jogos são compatíveis com esta opção, alguns requerem renderização entrelaçada internamente. Normalmente é seguro ativar..</u></b> - + Uses bilinear texture filtering when displaying the console's framebuffer to the screen. <br>Disabling filtering will producer a sharper, blockier/pixelated image. Enabling will smooth out the image. <br>The option will be less noticable the higher the resolution scale. Usa textura bilinear filtrando todo buffer para a tela principal.Desabilitar esta filtragem produzirá uma imagem mais nítida porém pixelada. Ativar irá deixar a imagem mais suave. Esta opção fica menos notável em resoluções mais altas. - + Adds padding to the display area to ensure that the ratio between pixels on the host to pixels in the console is an integer number. <br>May result in a sharper image in some 2D games. Adiciona preenchimento na tela para garantir que a proporção entre pixels seja um número inteiro. Pode resultar em uma imagem mais nítida em alguns jogos 2D. - + Enable this option to match DuckStation's refresh rate with your current monitor or screen. VSync is automatically disabled when it is not possible (e.g. running at non-100% speed). Habilite esta opção para combinar a taxa de atualização do emulador com seu monitor. O V-Sync (sincronização vertical) será desativado automaticamente quando não for possivel atingir 100% da velocidade. - + Setting this beyond 1x will enhance the resolution of rendered 3D polygons and lines. Only applies to the hardware backends. <br>This option is usually safe, with most games looking fine at higher resolutions. Higher resolutions require a more powerful GPU. Aumentar a resolução para mais de 1x aumentará a resolução dos Poligonos e linhas em jogos 3D. Só é utilizável quando usado com placas de video dedicadas. <br> Geralmente é seguro ativar esta opção, deixando assim a maior parte dos jogos com vizual muito melhor em resoluções mais altas; Porém, utiliza mais da sua placa de Vídeo. - + Scales the dither pattern to the resolution scale of the emulated GPU. This makes the dither pattern much less obvious at higher resolutions. <br>Usually safe to enable, and only supported by the hardware renderers. Escalona os 'ditherings' - pontilhados na imagem para a placa de Video.Torna a visão destes pontos muito menos visiveis em resoluções mais altas.Geralmente seguro ativar e suportado apenas pelos rederizadores por Hardware (ou seja usando sua placa de vídeo). - + Uses NTSC frame timings when the console is in PAL mode, forcing PAL games to run at 60hz. <br>For most games which have a speed tied to the framerate, this will result in the game running approximately 17% faster. <br>For variable frame rate games, it may not affect the speed. Quando o console está no modo PAL - Geralmente jogos Europeus rodam a 50hz. força estes jogos a rodar em até 60hz sendo assim, resulta em um jogo mais rápido até 15%.Em jogos com taxas de quadro (fps) variável pode isto não afetará a velocidade na hora da jogatina. - + Smooths out the blockyness of magnified textures on 3D object by using bilinear filtering. <br>Will have a greater effect on higher resolution scales. Only applies to the hardware renderers. Suaviza texturas ampliadas em objetos 3D usando filtragem bilinear. Terá efeito maior em resoluções mais altas. Aplica-se apenas aos rederizadores por hardware. - + Scales vertex positions in screen-space to a widescreen aspect ratio, essentially increasing the field of view from 4:3 to 16:9 in 3D games. <br>For 2D games, or games which use pre-rendered backgrounds, this enhancement will not work as expected. <br><b><u>May not be compatible with all games.</u></b> Escala as posições de vértices para uma proporção de aspecto esticado, aumentando o campo de visão de 4:3 para 16:9 em jogos 3D. <br>Para jogos 2D, ou jogos que usam fundos pré-rederizados, este aprimoramento não funcionará como esperado. <b><u>Pode não ser compatível com todos os jogos</u></b> - + Reduces "wobbly" polygons and "warping" textures that are common in PS1 games. <br>Only works with the hardware renderers. <b><u>May not be compatible with all games.</u></b> Reduz "tremeliques" nos poligonos tentando preservar os mesmos na hora da transferência para a memória. Funciona apenas se rederizado por hardware e pode não ser compatível com todos os jogos.</u></b> - + Increases the precision of polygon culling, reducing the number of holes in geometry. Requires geometry correction enabled. Aumenta a precisão das curvas nos poligonos, reduzindo o número de buracos na geometria do mesmo. Requer a Correção Geometrica ativada. - + Uses perspective-correct interpolation for texture coordinates and colors, straightening out warped textures. Requires geometry correction enabled. Utiliza interpolação corretiva em perspetiva para cordenadas e das cores na textura, endireitando as que estiverem distorcidas. Requer correção de geometria ativada. - + Uses screen coordinates as a fallback when tracking vertices through memory fails. May improve PGXP compatibility. Quando a correção de vertices falha, essa opção se encarrega de usar as coordenadas da tela para o rastreamento. Pode melhorar a compatibilidade com o PGXP. - + Tries to track vertex manipulation through the CPU. Some games require this option for PGXP to be effective. Very slow, and incompatible with the recompiler. Tenta manipular o rastreamento dos vértices (extremidades) direto para o processador. Alguns jogos exigem esta opção para que o aprimoramento - PGXP. tenha o efeito desejado. Atenção, este modo é MUITO LENTO, e imcompativel com o recompilador se ativo. - (for 720p) - >(720p) + >(720p) - (for 1080p) - >(1080p) + >(1080p) - (for 1440p) - >(1440p) + >(1440p) - (for 4K) - >(4k) + >(4k) - + Automatic based on window size Automático, baseado no tamanho da janela aberta - + + 1x + 100% {1x?} + + + + 2x + 100% {2x?} + + + + 3x (for 720p) + 3x > (720p) + + + + 4x + 100% {4x?} + + + + 5x (for 1080p) + 5x > (1080p) + + + + 6x (for 1440p) + 6x > (1440p) + + + + 7x + 100% {7x?} + + + + 8x + 100% {8x?} + + + + 9x + 100% {9x?} + + + + 10x + 100% {10x?} + + + + 11x + 100% {11x?} + + + + 12x + 100% {12x?} + + + + 13x + 100% {13x?} + + + + 14x + 100% {14x?} + + + + 15x + 100% {15x?} + + + + 16x + 100% {16x?} + + %1x%2 - %1x%2 + %1x%2 %1x (%2x%3 VRAM) %1x (%2x%3 VRAM) - - + + (Default) Padrão @@ -1392,32 +1479,32 @@ GameListCompatibilityRating - + Unknown Desconhecido - + Doesn't Boot Não Funciona - + Crashes In Intro Quebra logo no Inicio - + Crashes In-Game Quebra durante o Jogo - + Graphical/Audio Issues - Problemas de Aúdio e Vídeo + Problemas de Áudio e Vídeo - + No Issues Sem Problemas @@ -1425,37 +1512,37 @@ GameListModel - + Type Tipo - + Code Código - + Title Titulo - + File Title Titulo do Jogo (Na pasta) - + Size Tamanho - + Region Região - + Compatibility Compatibilidade @@ -1669,82 +1756,162 @@ This will download approximately 4 megabytes over your current internet connecti Configurações Personalizadas - GPU Settings - Configurações da GPU + Configurações da GPU - + Crop Mode: Modo de Corte: - + Aspect Ratio: Proporção e Aspecto: + + + GPU Screen Display + Modo de Exibição GPU + + Linear Upscaling + Escalonamento Linear + + + + Integer Upscaling + Escalonamento Integro + + + + GPU Enhancements + Melhorias GPU + + + + Resolution Scale: + Escala de Resolução: + + + + True Color Rendering (24-bit, disables dithering) + Renderização em (24 Cores, desativa o efeito dithering) + + + + Scaled Dithering (scale dither pattern to resolution) + Dithering Escalonado, (Escalona o padrão do dithering para a resolução) + + + Widescreen Hack Melhoria para Telas Panorâmicas - + + Force NTSC Timings (60hz-on-PAL) + Força o temporizador NTSC (60hz Jogos EU) + + + + Bilinear Texture Filtering + Filtragem de Textura Bilinear + + + + PGXP Geometry Correction + PGXP Correção Geometrica + + + Controller Settings Configurações de Controle - + Controller 1 Type: Opção Controle 1: - + Controller 2 Type: Opção Controle 2: - + + Memory Card Settings + Cartões de Memória + + + + Memory Card 1 Type: + Cartão de Memória Tipo 1: + + + + Memory Card 1 Shared Path: + Cartão de Memória 1 Caminho do Compartilhamento: + + + + + Browse... + Procurar... + + + + Memory Card 2 Type: + Cartão de Memória Tipo 2: + + + + Memory Card 2 Shared Path: + Cartão de Memória 2 Caminho do Compartilhamento: + + + Compatibility Settings Configurações de Compatibilidade - + Traits Caracteristicas Individuais - + Overrides Sobreposições - + Display Active Offset: Opções de Deslocamento: - + Compute Hashes Calcular Valores - + Verify Dump Validar Jogo - + Export Compatibility Info Exportar Informação de Compatibilidade - + Close Fechar - + Game Properties - %1 Propriedades do Jogo - %1 @@ -1753,30 +1920,39 @@ This will download approximately 4 megabytes over your current internet connecti %1 - - - + + + + + + (unchanged) (Inalterado) - + <not computed> <não calculado> - + + + Select path to memory card image + Escolha o caminho para os Cartões de Memória + + + Not yet implemented Não Implementado Ainda - + Compatibility Info Export - + Press OK to copy to clipboard. Dê ok para copiar para área de transferência. @@ -1784,69 +1960,95 @@ This will download approximately 4 megabytes over your current internet connecti GameSettingsTrait - + Force Interpreter Forçar Interpretador - + Force Software Renderer Forçar Renderização por Software - Enable Interlacing - Ativar Entrelaçamento + Ativar Entrelaçamento - + + Force Interlacing + Força o Entrelaçamento + + + Disable True Color Desativar Cor Real (True Color) - + Disable Upscaling Desativar Escalonamento - + Disable Scaled Dithering Desativar Escalonamento do Dithering - + + Disallow Forcing NTSC Timings + Desativa os temporizadores em NTSC + + + Disable Widescreen Desativar Func.Esticar (Widescreen) - + Disable PGXP Desativar PGXP - + Disable PGXP Culling Desativar Correção de Curvas - + + Force PGXP Vertex Cache + Força o armazenamento de cache em modo PGXP + + + + Force PGXP CPU Mode + Força o PGXP em modo CPU + + + + Force Recompiler Memory Exceptions + Forçar exceções de memória do recompilador + + + + Force Recompiler ICache + Força Recompilador em modo Armazenado (ICache) + + Enable PGXP Vertex Cache - Ativar PGXP Vértice Armazenado + Ativar PGXP Vértice Armazenado - Enable PGXP CPU Mode - Ativar PGXP - Modo CPU + Ativar PGXP - Modo CPU - + Force Digital Controller Forçar Controle Digital (D-Pad) - Enable Recompiler Memory Exceptions - Habilitar Exceções de Memória + Habilitar Exceções de Memória @@ -2112,112 +2314,112 @@ This will download approximately 4 megabytes over your current internet connecti Hotkeys - + Fast Forward Avanço Rápido - + Toggle Fast Forward Pulo de Quadros (Alternado) - + Toggle Fullscreen Tela Cheia - + Toggle Pause Pausa - + Power Off System Desligar o Sistema - + Save Screenshot Salvar Caputra de tela - + Frame Step Pulo de quadro (Fixo) - + Toggle Software Rendering Alternar para Renderizador por Software - + Toggle PGXP PGXP - + Increase Resolution Scale Aumentar Escala de Resolução - + Decrease Resolution Scale Diminuir Escala de Resolução - + Load From Selected Slot Carregar do Estado Salvo - + Save To Selected Slot Salvar para compartimento Selecionado - + Select Previous Save Slot Selecionar compartimento anterior - + Select Next Save Slot Selecionar próximo compartimento - + Load Game State %u Carregar estado de jogo %u - + Save Game State %u Salvar Estado do Jogo %u - + Load Global State %u Carregar Estado Global %u - + Save Global State %u Salvar Estado Global %u - + Toggle Mute Mudo - + Volume Up Volume + - + Volume Down Volume - @@ -2255,8 +2457,8 @@ This will download approximately 4 megabytes over your current internet connecti Atribuições para %1 %2 - - + + Push Button/Axis... [%1] Aperte Botão/Analogicos... [%1] @@ -2269,8 +2471,8 @@ This will download approximately 4 megabytes over your current internet connecti %1 atribuições - - + + Push Button/Axis... [%1] Aperte Botão/Analogicos... [%1] @@ -2278,52 +2480,52 @@ This will download approximately 4 megabytes over your current internet connecti LogLevel - + None Nenhum - + Error Erro - + Warning Atenção - + Performance Performance - + Success Sucesso - + Information Informação - + Developer Desenvolvedor - + Profile Perfil - + Debug Depurar - + Trace Rastreio @@ -2762,6 +2964,7 @@ This will download approximately 4 megabytes over your current internet connecti MemoryCardSettingsWidget + All Memory Card Types (*.mcd *.mcr *.mc) Todos os Tipos de MC (*.mcd *.mcr *.mc) @@ -2826,22 +3029,22 @@ This will download approximately 4 megabytes over your current internet connecti MemoryCardType - + No Memory Card Sem Cartão de Memória - + Shared Between All Games Compartrilhada Entre Jogos - + Separate Card Per Game (Game Code) Separar Cartão Por Jogo (Cód. Jogo) - + Separate Card Per Game (Game Title) Separar Cartão Por Jogo (Titulo. Jogo) @@ -2867,97 +3070,172 @@ This will download approximately 4 megabytes over your current internet connecti OSDMessage - + System reset. Sistema Reiniciado. - + Loading state from '%s'... Carregando estado de '%s'... - + Loading state from '%s' failed. Resetting. Carregamento de estado '%s'.falhou. Reiniciando. - + Saving state to '%s' failed. Salvando estado para '%s' falhou. - + State saved to '%s'. Estado salvo para '%s'. - + PGXP is incompatible with the software renderer, disabling PGXP. PGXP é incompatível com o rederizador por software, desativando PGXP. - + PGXP CPU mode is incompatible with the recompiler, using Cached Interpreter instead. PGXP em modo CPU não é compatível com o recompilador, mudando para Interpretador armazenado. - + Speed limiter enabled. Limitador de Velocidade Ativado. - + Speed limiter disabled. Limitador de Velocidade Desativado. - + Volume: Muted Volume: Mudo - - - + + + Volume: %d%% Volume: %d%% - + Loaded input profile from '%s' Perfil de controle carregado de '%s' - + Failed to save screenshot to '%s' Falha ao salvar captura para '%s' - + Screenshot saved to '%s'. Captura de tela salva para '%s'. + + + CPU interpreter forced by game settings. + Configurado o interpretador por CPU pela configuração personalizada. + + + + Software renderer forced by game settings. + Renderização por software forçada pelas configurações personalizadas. + + + + Interlacing forced by game settings. + Entrelaçamento forçado pela configuração personalizada. + + + + True color disabled by game settings. + Efeito Cor real (true color) desativada pelas configs. personalizadas. + + + + Upscaling disabled by game settings. + Escalonamento desativado pelas configurações personalizadas. + + + + Scaled dithering disabled by game settings. + Dithering escalonado desativado pelas configurações personalizadas. + + + + Widescreen disabled by game settings. + Visão Panoramica desativada pelas configurações. + + + + Forcing NTSC Timings disallowed by game settings. + Temporizadores NTSC não permitidos pela configuração personalizada. + + + + PGXP geometry correction disabled by game settings. + Correção geométrica desativada pelas configurações personalizadas. + + + + PGXP culling disabled by game settings. + Correção de curvas desativada pela configuração personalizada. + + + + PGXP vertex cache forced by game settings. + Vertice Armazenado forçado pelas configurações personalizadas. + + + + PGXP CPU mode forced by game settings. + PGXP em modo CPU forçado pelas configurações personalizadas. + + + + Controller %u changed to digital by game settings. + Controle %u mudado para modo analogico pela configuração personalizada. + + + + Recompiler memory exceptions forced by game settings. + Exeções de RAM forçada pelas configurações. + + + + Recompiler ICache forced by game settings. + Recompilador ICache forçado pelas configurações. + QObject - + DuckStation Error Erro no Duckstation - + Failed to initialize host interface. Cannot continue. Falha ao Iniciar Interface. Não é possivel Continuar. - + Failed to open URL Falha ao abrir Página - + Failed to open URL. The URL was: %1 @@ -2967,62 +3245,62 @@ The URL was: %1 QtHostInterface - + Game Save %1 (%2) Jogo Salvo %1 (%2) - + Game Save %1 (Empty) Jogo Salvo %1 (Vazio) - + Global Save %1 (%2) Compartimento Global %1 (%2) - + Global Save %1 (Empty) Compartimento Global %1 (Vazio) - + Resume Resumir - + Load State Carregar Estado - + Resume (%1) Resumir (%1) - + %1 Save %2 (%3) %1 Salvo %2 (%3) - + Game Jogo - + Delete Save States... Apagar Jogos Salvos... - + Confirm Save State Deletion Confirma deleção de Estado Salvo - + Are you sure you want to delete all save states for %1? The saves will not be recoverable. @@ -3102,7 +3380,7 @@ The saves will not be recoverable. Audio Settings - Configurações de Aúdio + Configurações de Áudio @@ -3147,7 +3425,7 @@ The saves will not be recoverable. <strong>Audio Settings</strong><hr>These options control the audio output of the console. Mouse over an option for additional information. - <strong>Configurações de Aúdio</strong><hr>Estas opções controlam a saída do som no emulador. passe o ponteiro do mouse para mais informações. + <strong>Configurações de Áudio</strong><hr>Estas opções controlam a saída do som no emulador. passe o ponteiro do mouse para mais informações. @@ -3163,32 +3441,32 @@ The saves will not be recoverable. System - + Save state is incompatible: expecting version %u but state is version %u. Estado salvo incompatível: versão do mesmo esperada %u não a versão %u. - + Failed to open CD image from save state: '%s'. Falha ao abrir estado salvo: '%s'. - + Per-game memory card cannot be used for slot %u as the running game has no code. Using shared card instead. Caminho para o Cartão de Memória no compartimento %u não pôde ser usado pois o jogo iniciado não possui um cód. válido. Será usado cartão compartilhado. - + Per-game memory card cannot be used for slot %u as the running game has no title. Using shared card instead. Caminho para o Cartão de Memória no compartimento %u não pôde ser usado pois o jogo iniciado não possui um nome. válido. Será usado cartão compartilhado. - + Memory card path for slot %u is missing, using default. Caminho para o Cartão de Memória %u incorreto, usando o padrão. - + Game changed, reloading memory cards. Jogo trocado, recarregando Cartões de Memória. From a2eaaf0e8942624d344855da713645cda2386a22 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Wed, 2 Sep 2020 22:44:52 +1000 Subject: [PATCH 48/61] PGXP: Allocate large storage dynamically This causes performance issues on ARM otherwise. --- src/core/cpu_core.cpp | 4 +- src/core/host_interface.cpp | 3 + src/core/pgxp.cpp | 118 +++++++++++------- src/core/pgxp.h | 1 + src/frontend-common/common_host_interface.cpp | 2 + 5 files changed, 83 insertions(+), 45 deletions(-) diff --git a/src/core/cpu_core.cpp b/src/core/cpu_core.cpp index 3dd02cd70..cb92a8799 100644 --- a/src/core/cpu_core.cpp +++ b/src/core/cpu_core.cpp @@ -61,6 +61,7 @@ void Initialize() void Shutdown() { // GTE::Shutdown(); + PGXP::Shutdown(); } void Reset() @@ -128,7 +129,8 @@ bool DoState(StateWrapper& sw) if (sw.IsReading()) { ClearICache(); - PGXP::Initialize(); + if (g_settings.gpu_pgxp_enable) + PGXP::Initialize(); } return !sw.HasError(); diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index 340c87715..bbf1c0990 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -561,6 +561,9 @@ void HostInterface::CheckForSettingsChanges(const Settings& old_settings) CPU::CodeCache::Flush(); } + if (old_settings.gpu_pgxp_enable) + PGXP::Shutdown(); + if (g_settings.gpu_pgxp_enable) PGXP::Initialize(); } diff --git a/src/core/pgxp.cpp b/src/core/pgxp.cpp index 8fef9c7d9..029f6057e 100644 --- a/src/core/pgxp.cpp +++ b/src/core/pgxp.cpp @@ -20,8 +20,8 @@ #include "pgxp.h" #include "settings.h" -#include #include +#include namespace PGXP { // pgxp_types.h @@ -134,6 +134,26 @@ static void WriteMem(PGXP_value* value, u32 addr); static void WriteMem16(PGXP_value* src, u32 addr); // pgxp_gpu.h +enum : u32 +{ + VERTEX_CACHE_WIDTH = 0x800 * 2, + VERTEX_CACHE_HEIGHT = 0x800 * 2, + VERTEX_CACHE_SIZE = VERTEX_CACHE_WIDTH * VERTEX_CACHE_HEIGHT, + PGXP_MEM_SIZE = 3 * 2048 * 1024 / 4 // mirror 2MB in 32-bit words * 3 +}; + +static PGXP_value* Mem = nullptr; + +const unsigned int mode_init = 0; +const unsigned int mode_write = 1; +const unsigned int mode_read = 2; +const unsigned int mode_fail = 3; + +unsigned int baseID = 0; +unsigned int lastID = 0; +unsigned int cacheMode = 0; +static PGXP_value* vertexCache = nullptr; + void PGXP_CacheVertex(short sx, short sy, const PGXP_value* _pVertex); // pgxp_gte.h @@ -195,7 +215,6 @@ double f16Overflow(double in) // pgxp_mem.c static void PGXP_InitMem(); -static PGXP_value Mem[3 * 2048 * 1024 / 4]; // mirror 2MB in 32-bit words * 3 static const u32 UserMemOffset = 0; static const u32 ScratchOffset = 2048 * 1024 / 4; static const u32 RegisterOffset = 2 * 2048 * 1024 / 4; @@ -203,7 +222,19 @@ static const u32 InvalidAddress = 3 * 2048 * 1024 / 4; void PGXP_InitMem() { - memset(Mem, 0, sizeof(Mem)); + if (!Mem) + { + Mem = static_cast(std::calloc(PGXP_MEM_SIZE, sizeof(PGXP_value))); + if (!Mem) + { + std::fprintf(stderr, "Failed to allocate PGXP memory\n"); + std::abort(); + } + } + else + { + std::memset(Mem, 0, sizeof(PGXP_value) * PGXP_MEM_SIZE); + } } u32 PGXP_ConvertAddress(u32 addr) @@ -366,8 +397,6 @@ void WriteMem16(PGXP_value* src, u32 addr) } // pgxp_main.c -u32 static gMode = 0; - void Initialize() { PGXP_InitMem(); @@ -375,24 +404,19 @@ void Initialize() PGXP_InitGTE(); } -void PGXP_SetModes(u32 modes) +void Shutdown() { - gMode = modes; -} - -u32 PGXP_GetModes() -{ - return gMode; -} - -void PGXP_EnableModes(u32 modes) -{ - gMode |= modes; -} - -void PGXP_DisableModes(u32 modes) -{ - gMode = gMode & ~modes; + cacheMode = mode_init; + if (vertexCache) + { + std::free(vertexCache); + vertexCache = nullptr; + } + if (Mem) + { + std::free(Mem); + Mem = nullptr; + } } // pgxp_gte.c @@ -584,17 +608,6 @@ void CPU_SWC2(u32 instr, u32 rtVal, u32 addr) ///////////////////////////////// //// Blade_Arma's Vertex Cache (CatBlade?) ///////////////////////////////// -const unsigned int mode_init = 0; -const unsigned int mode_write = 1; -const unsigned int mode_read = 2; -const unsigned int mode_fail = 3; - -PGXP_value vertexCache[0x800 * 2][0x800 * 2]; - -unsigned int baseID = 0; -unsigned int lastID = 0; -unsigned int cacheMode = 0; - unsigned int IsSessionID(unsigned int vertID) { // No wrapping @@ -612,6 +625,23 @@ unsigned int IsSessionID(unsigned int vertID) return 0; } +static void InitPGXPVertexCache() +{ + if (!vertexCache) + { + vertexCache = static_cast(std::calloc(VERTEX_CACHE_SIZE, sizeof(PGXP_value))); + if (!vertexCache) + { + std::fprintf(stderr, "Failed to allocate PGXP vertex cache memory\n"); + std::abort(); + } + } + else + { + memset(vertexCache, 0x00, VERTEX_CACHE_SIZE * sizeof(PGXP_value)); + } +} + void PGXP_CacheVertex(short sx, short sy, const PGXP_value* _pVertex) { const PGXP_value* pNewVertex = (const PGXP_value*)_pVertex; @@ -623,14 +653,14 @@ void PGXP_CacheVertex(short sx, short sy, const PGXP_value* _pVertex) return; } + // Initialise cache on first use + if (!vertexCache) + InitPGXPVertexCache(); + // if (bGteAccuracy) { if (cacheMode != mode_write) { - // Initialise cache on first use - if (cacheMode == mode_init) - memset(vertexCache, 0x00, sizeof(vertexCache)); - // First vertex of write session (frame?) cacheMode = mode_write; baseID = pNewVertex->count; @@ -640,7 +670,7 @@ void PGXP_CacheVertex(short sx, short sy, const PGXP_value* _pVertex) if (sx >= -0x800 && sx <= 0x7ff && sy >= -0x800 && sy <= 0x7ff) { - pOldVertex = &vertexCache[sy + 0x800][sx + 0x800]; + pOldVertex = &vertexCache[(sy + 0x800) * VERTEX_CACHE_WIDTH + (sx + 0x800)]; // To avoid ambiguity there can only be one valid entry per-session if (0) //(IsSessionID(pOldVertex->count) && (pOldVertex->value == pNewVertex->value)) @@ -664,25 +694,25 @@ void PGXP_CacheVertex(short sx, short sy, const PGXP_value* _pVertex) PGXP_value* PGXP_GetCachedVertex(short sx, short sy) { - // if (bGteAccuracy) + if (g_settings.gpu_pgxp_vertex_cache) { if (cacheMode != mode_read) { if (cacheMode == mode_fail) return NULL; - // Initialise cache on first use - if (cacheMode == mode_init) - memset(vertexCache, 0x00, sizeof(vertexCache)); - // First vertex of read session (frame?) cacheMode = mode_read; } + // Initialise cache on first use + if (!vertexCache) + InitPGXPVertexCache(); + if (sx >= -0x800 && sx <= 0x7ff && sy >= -0x800 && sy <= 0x7ff) { // Return pointer to cache entry - return &vertexCache[sy + 0x800][sx + 0x800]; + return &vertexCache[(sy + 0x800) * VERTEX_CACHE_WIDTH + (sx + 0x800)]; } } diff --git a/src/core/pgxp.h b/src/core/pgxp.h index db3192cd4..e5c664941 100644 --- a/src/core/pgxp.h +++ b/src/core/pgxp.h @@ -24,6 +24,7 @@ namespace PGXP { void Initialize(); +void Shutdown(); // -- GTE functions // Transforms diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 93a332774..25acb6286 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -1411,6 +1411,8 @@ void CommonHostInterface::RegisterGraphicsHotkeys() if (g_settings.gpu_pgxp_enable) PGXP::Initialize(); + else + PGXP::Shutdown(); // we need to recompile all blocks if pgxp is toggled on/off if (g_settings.IsUsingCodeCache()) From 42d49426e8962924ace0942db76aa47b4b622b12 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Thu, 3 Sep 2020 00:10:27 +1000 Subject: [PATCH 49/61] CPU/Recompiler/AArch64: Fix crash when calling >26-bit away functions --- .../cpu_recompiler_code_generator_aarch64.cpp | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/core/cpu_recompiler_code_generator_aarch64.cpp b/src/core/cpu_recompiler_code_generator_aarch64.cpp index de2f8ec3c..4a62184ab 100644 --- a/src/core/cpu_recompiler_code_generator_aarch64.cpp +++ b/src/core/cpu_recompiler_code_generator_aarch64.cpp @@ -18,6 +18,7 @@ constexpr HostReg RARG1 = 0; constexpr HostReg RARG2 = 1; constexpr HostReg RARG3 = 2; constexpr HostReg RARG4 = 3; +constexpr HostReg RSCRATCH = 8; constexpr u64 FUNCTION_CALL_STACK_ALIGNMENT = 16; constexpr u64 FUNCTION_CALL_SHADOW_SPACE = 32; constexpr u64 FUNCTION_CALLEE_SAVED_SPACE_RESERVE = 80; // 8 registers @@ -125,7 +126,7 @@ void CodeGenerator::InitHostRegs() // TODO: function calls mess up the parameter registers if we use them.. fix it // allocate nonvolatile before volatile m_register_cache.SetHostRegAllocationOrder( - {19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}); + {19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17}); m_register_cache.SetCallerSavedHostRegs({0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}); m_register_cache.SetCalleeSavedHostRegs({19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 30}); m_register_cache.SetCPUPtrHostReg(RCPUPTR); @@ -977,8 +978,8 @@ void CodeGenerator::EmitFunctionCallPtr(Value* return_value, const void* ptr) const bool use_blr = !vixl::IsInt26(displacement); if (use_blr) { - m_emit->Mov(GetHostReg64(RRETURN), reinterpret_cast(ptr)); - m_emit->Blr(GetHostReg64(RRETURN)); + m_emit->Mov(GetHostReg64(RSCRATCH), reinterpret_cast(ptr)); + m_emit->Blr(GetHostReg64(RSCRATCH)); } else { @@ -1012,8 +1013,8 @@ void CodeGenerator::EmitFunctionCallPtr(Value* return_value, const void* ptr, co const bool use_blr = !vixl::IsInt26(displacement); if (use_blr) { - m_emit->Mov(GetHostReg64(RRETURN), reinterpret_cast(ptr)); - m_emit->Blr(GetHostReg64(RRETURN)); + m_emit->Mov(GetHostReg64(RSCRATCH), reinterpret_cast(ptr)); + m_emit->Blr(GetHostReg64(RSCRATCH)); } else { @@ -1048,8 +1049,8 @@ void CodeGenerator::EmitFunctionCallPtr(Value* return_value, const void* ptr, co const bool use_blr = !vixl::IsInt26(displacement); if (use_blr) { - m_emit->Mov(GetHostReg64(RRETURN), reinterpret_cast(ptr)); - m_emit->Blr(GetHostReg64(RRETURN)); + m_emit->Mov(GetHostReg64(RSCRATCH), reinterpret_cast(ptr)); + m_emit->Blr(GetHostReg64(RSCRATCH)); } else { @@ -1086,8 +1087,8 @@ void CodeGenerator::EmitFunctionCallPtr(Value* return_value, const void* ptr, co const bool use_blr = !vixl::IsInt26(displacement); if (use_blr) { - m_emit->Mov(GetHostReg64(RRETURN), reinterpret_cast(ptr)); - m_emit->Blr(GetHostReg64(RRETURN)); + m_emit->Mov(GetHostReg64(RSCRATCH), reinterpret_cast(ptr)); + m_emit->Blr(GetHostReg64(RSCRATCH)); } else { @@ -1125,8 +1126,8 @@ void CodeGenerator::EmitFunctionCallPtr(Value* return_value, const void* ptr, co const bool use_blr = !vixl::IsInt26(displacement); if (use_blr) { - m_emit->Mov(GetHostReg64(RRETURN), reinterpret_cast(ptr)); - m_emit->Blr(GetHostReg64(RRETURN)); + m_emit->Mov(GetHostReg64(RSCRATCH), reinterpret_cast(ptr)); + m_emit->Blr(GetHostReg64(RSCRATCH)); } else { From fd0a009a7fe5207a83f16d02d9f8f7d9305feb7f Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Thu, 3 Sep 2020 12:25:59 +1000 Subject: [PATCH 50/61] System: Add missing include --- src/core/system.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/system.cpp b/src/core/system.cpp index 3d4630663..afa06785e 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -27,6 +27,7 @@ #include "spu.h" #include "timers.h" #include +#include #include #include Log_SetChannel(System); From 89e0290d06af703bd42cdd207c9056456a427f37 Mon Sep 17 00:00:00 2001 From: Silent Date: Fri, 4 Sep 2020 23:44:19 +0200 Subject: [PATCH 51/61] Fix resource leaks in AutoStagingTexture::EnsureSize and D3D11HostDisplay::DownloadTexture --- src/common/d3d11/staging_texture.cpp | 4 ++-- src/frontend-common/d3d11_host_display.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/common/d3d11/staging_texture.cpp b/src/common/d3d11/staging_texture.cpp index 148251308..a90a3f365 100644 --- a/src/common/d3d11/staging_texture.cpp +++ b/src/common/d3d11/staging_texture.cpp @@ -84,8 +84,8 @@ bool AutoStagingTexture::EnsureSize(ID3D11DeviceContext* context, u32 width, u32 if (m_texture && m_width >= width && m_height >= height && m_format == format) return true; - ID3D11Device* device; - context->GetDevice(&device); + ComPtr device; + context->GetDevice(device.GetAddressOf()); CD3D11_TEXTURE2D_DESC new_desc(format, width, height, 1, 1, 0, for_uploading ? D3D11_USAGE_DYNAMIC : D3D11_USAGE_STAGING, diff --git a/src/frontend-common/d3d11_host_display.cpp b/src/frontend-common/d3d11_host_display.cpp index b8ec7fe58..80162f505 100644 --- a/src/frontend-common/d3d11_host_display.cpp +++ b/src/frontend-common/d3d11_host_display.cpp @@ -156,15 +156,15 @@ bool D3D11HostDisplay::DownloadTexture(const void* texture_handle, u32 x, u32 y, { ID3D11ShaderResourceView* srv = const_cast(static_cast(texture_handle)); - ID3D11Resource* srv_resource; + ComPtr srv_resource; D3D11_SHADER_RESOURCE_VIEW_DESC srv_desc; - srv->GetResource(&srv_resource); + srv->GetResource(srv_resource.GetAddressOf()); srv->GetDesc(&srv_desc); if (!m_readback_staging_texture.EnsureSize(m_context.Get(), width, height, srv_desc.Format, false)) return false; - m_readback_staging_texture.CopyFromTexture(m_context.Get(), srv_resource, 0, x, y, 0, 0, width, height); + m_readback_staging_texture.CopyFromTexture(m_context.Get(), srv_resource.Get(), 0, x, y, 0, 0, width, height); return m_readback_staging_texture.ReadPixels(m_context.Get(), 0, 0, width, height, out_data_stride / sizeof(u32), static_cast(out_data)); } From b117326d598029c4c99ef19bf580298fba7d3f55 Mon Sep 17 00:00:00 2001 From: Anderson_Cardoso <43047877+andercard0@users.noreply.github.com> Date: Fri, 4 Sep 2020 19:57:54 -0300 Subject: [PATCH 52/61] Quick update - Pt-Br - Again Cosmetic stuff - nothing really important. --- .../translations/duckstation-qt_pt-br.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/duckstation-qt/translations/duckstation-qt_pt-br.ts b/src/duckstation-qt/translations/duckstation-qt_pt-br.ts index fb62dc8f0..0cb7e6515 100644 --- a/src/duckstation-qt/translations/duckstation-qt_pt-br.ts +++ b/src/duckstation-qt/translations/duckstation-qt_pt-br.ts @@ -1384,82 +1384,82 @@ 1x - 100% {1x?} + 1x > (1024x512) 2x - 100% {2x?} + 2x > (2048x1024) 3x (for 720p) - 3x > (720p) + 3x > (3072x1536) 4x - 100% {4x?} + 4x > (4096x2048) 5x (for 1080p) - 5x > (1080p) + 5x > (5120x2560) 6x (for 1440p) - 6x > (1440p) + 6x >(6144x3072) 7x - 100% {7x?} + 7x > (7168x3584) 8x - 100% {8x?} + 8x > (8192x4096) 9x - 100% {9x?} + 9x > (9216x4608) 10x - 100% {10x?} + 10x > (10240x5120) 11x - 100% {11x?} + 11x > (11264x5632) 12x - 100% {12x?} + 12x > (12288x6144) 13x - 100% {13x?} + 13x > (13312x6656) 14x - 100% {14x?} + 14x > (14336x7168) 15x - 100% {15x?} + 15x > (15360x7680) 16x - 100% {16x?} + 16x > (16384x8192) %1x%2 From 33df4ab1bdec175a9b28c51b86d8f15bacdc8104 Mon Sep 17 00:00:00 2001 From: Silent Date: Sat, 5 Sep 2020 21:46:46 +0200 Subject: [PATCH 53/61] Seek before reading save state screenshot Fixes corrupted thumbnails in the save state UI --- src/frontend-common/common_host_interface.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 25acb6286..74f834644 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -1820,6 +1820,7 @@ CommonHostInterface::GetExtendedSaveStateInfo(const char* game_code, s32 slot) if (header.screenshot_width > 0 && header.screenshot_height > 0 && header.screenshot_size > 0 && (static_cast(header.offset_to_screenshot) + static_cast(header.screenshot_size)) <= stream->GetSize()) { + stream->SeekAbsolute(header.offset_to_screenshot); ssi.screenshot_data.resize((header.screenshot_size + 3u) / 4u); if (stream->Read2(ssi.screenshot_data.data(), header.screenshot_size)) { From 2565d2cae125ae88b9e6355e5c49c489bb1a523e Mon Sep 17 00:00:00 2001 From: Silent Date: Sat, 5 Sep 2020 23:01:07 +0200 Subject: [PATCH 54/61] Ass a 256x256 app icon --- src/duckstation-qt/duckstation-qt.ico | Bin 99678 -> 113683 bytes src/duckstation-sdl/duckstation-sdl.ico | Bin 99678 -> 113683 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/duckstation-qt/duckstation-qt.ico b/src/duckstation-qt/duckstation-qt.ico index 082a9d8b81d36e83a886188a363f33d180aca934..f95b67991a723da0ca79fe220bf9bbc9a4cbba31 100644 GIT binary patch delta 14289 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 delta 178 zcmbRIgY8}yTMh#QBLgdgfB*v!C@^HOFffFHSPBsS3ZVEtpqPOHl&{LbkS5B&;NSq^ zYdA45@L4l3G&Df@9E=QRwG0dzKrYBg1t4YwlRyT`WJgxHjk7#i7zKbVtIcw(xdMzT vlMjkTZGI-|tIFsAWR`7SWxdIfk#YOR62?=En;Y818Jq1Iw%au@8n6NY;yfV? diff --git a/src/duckstation-sdl/duckstation-sdl.ico b/src/duckstation-sdl/duckstation-sdl.ico index 082a9d8b81d36e83a886188a363f33d180aca934..f95b67991a723da0ca79fe220bf9bbc9a4cbba31 100644 GIT binary patch delta 14289 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 delta 178 zcmbRIgY8}yTMh#QBLgdgfB*v!C@^HOFffFHSPBsS3ZVEtpqPOHl&{LbkS5B&;NSq^ zYdA45@L4l3G&Df@9E=QRwG0dzKrYBg1t4YwlRyT`WJgxHjk7#i7zKbVtIcw(xdMzT vlMjkTZGI-|tIFsAWR`7SWxdIfk#YOR62?=En;Y818Jq1Iw%au@8n6NY;yfV? From ac6b9dc87fd5a4bfda1b01c00cb9ad0918d7cacb Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Fri, 4 Sep 2020 17:03:33 +1000 Subject: [PATCH 55/61] dep/imgui: Move implementations to frontend-common --- dep/imgui/CMakeLists.txt | 17 ----------- dep/imgui/imgui.vcxproj | 30 +++++-------------- dep/imgui/imgui.vcxproj.filters | 6 ---- src/frontend-common/CMakeLists.txt | 17 +++++++++++ src/frontend-common/frontend-common.vcxproj | 6 ++++ .../frontend-common.vcxproj.filters | 6 ++++ .../frontend-common}/imgui_impl_dx11.cpp | 0 .../frontend-common}/imgui_impl_dx11.h | 0 .../frontend-common}/imgui_impl_opengl3.cpp | 0 .../frontend-common}/imgui_impl_opengl3.h | 0 .../frontend-common}/imgui_impl_vulkan.cpp | 0 .../frontend-common}/imgui_impl_vulkan.h | 0 12 files changed, 37 insertions(+), 45 deletions(-) rename {dep/imgui/src => src/frontend-common}/imgui_impl_dx11.cpp (100%) rename {dep/imgui/include => src/frontend-common}/imgui_impl_dx11.h (100%) rename {dep/imgui/src => src/frontend-common}/imgui_impl_opengl3.cpp (100%) rename {dep/imgui/include => src/frontend-common}/imgui_impl_opengl3.h (100%) rename {dep/imgui/src => src/frontend-common}/imgui_impl_vulkan.cpp (100%) rename {dep/imgui/include => src/frontend-common}/imgui_impl_vulkan.h (100%) diff --git a/dep/imgui/CMakeLists.txt b/dep/imgui/CMakeLists.txt index 8f5b999f2..d87184ad5 100644 --- a/dep/imgui/CMakeLists.txt +++ b/dep/imgui/CMakeLists.txt @@ -18,20 +18,3 @@ target_include_directories(imgui PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include" " target_include_directories(imgui INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") target_compile_definitions(imgui PRIVATE "imgui_STATIC") -target_sources(imgui PRIVATE - include/imgui_impl_opengl3.h - src/imgui_impl_opengl3.cpp -) -target_link_libraries(imgui PRIVATE glad) - -target_sources(imgui PRIVATE - include/imgui_impl_vulkan.h - src/imgui_impl_vulkan.cpp -) -target_link_libraries(imgui PRIVATE vulkan-loader) - -if(WIN32) - target_sources(imgui PRIVATE include/imgui_impl_dx11.h src/imgui_impl_dx11.cpp) -endif() - - diff --git a/dep/imgui/imgui.vcxproj b/dep/imgui/imgui.vcxproj index 245fbbb3d..b88cad554 100644 --- a/dep/imgui/imgui.vcxproj +++ b/dep/imgui/imgui.vcxproj @@ -37,9 +37,6 @@ - - - @@ -49,20 +46,9 @@ - - - - - - {43540154-9e1e-409c-834f-b84be5621388} - - - {9c8ddeb0-2b8f-4f5f-ba86-127cdf27f035} - - {BB08260F-6FBC-46AF-8924-090EE71360C6} Win32Proj @@ -211,7 +197,7 @@ Disabled imgui_STATIC;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) ProgramDatabase - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) + $(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -231,7 +217,7 @@ Disabled imgui_STATIC;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) ProgramDatabase - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) + $(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -251,7 +237,7 @@ Disabled imgui_STATIC;WIN32;_ITERATOR_DEBUG_LEVEL=1;_DEBUG;_LIB;%(PreprocessorDefinitions) ProgramDatabase - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) + $(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -273,7 +259,7 @@ Disabled imgui_STATIC;WIN32;_ITERATOR_DEBUG_LEVEL=1;_DEBUG;_LIB;%(PreprocessorDefinitions) ProgramDatabase - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) + $(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -295,7 +281,7 @@ MaxSpeed true imgui_STATIC;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) + $(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) true /Zo /utf-8 %(AdditionalOptions) false @@ -318,7 +304,7 @@ MaxSpeed true imgui_STATIC;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) + $(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) true /Zo /utf-8 %(AdditionalOptions) true @@ -342,7 +328,7 @@ MaxSpeed true imgui_STATIC;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) + $(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) true /Zo /utf-8 %(AdditionalOptions) false @@ -365,7 +351,7 @@ MaxSpeed true imgui_STATIC;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) + $(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories) true /Zo /utf-8 %(AdditionalOptions) true diff --git a/dep/imgui/imgui.vcxproj.filters b/dep/imgui/imgui.vcxproj.filters index 1d6e671ff..0257cb6dd 100644 --- a/dep/imgui/imgui.vcxproj.filters +++ b/dep/imgui/imgui.vcxproj.filters @@ -1,8 +1,6 @@  - - @@ -10,15 +8,11 @@ - - - - \ No newline at end of file diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index b92984c92..91585ff6a 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -16,6 +16,23 @@ if(WIN32) endif() if(NOT BUILD_LIBRETRO_CORE) + target_sources(imgui PRIVATE + imgui_impl_opengl3.h + imgui_impl_opengl3.cpp + ) + + target_sources(imgui PRIVATE + imgui_impl_vulkan.h + imgui_impl_vulkan.cpp + ) + + if(WIN32) + target_sources(imgui PRIVATE + imgui_impl_dx11.h + imgui_impl_dx11.cpp + ) + endif() + target_sources(frontend-common PRIVATE common_host_interface.cpp common_host_interface.h diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index 8cfe596ea..22d7690d0 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -73,6 +73,9 @@ + + + @@ -90,6 +93,9 @@ + + + diff --git a/src/frontend-common/frontend-common.vcxproj.filters b/src/frontend-common/frontend-common.vcxproj.filters index 5efdfa632..8b9148ca3 100644 --- a/src/frontend-common/frontend-common.vcxproj.filters +++ b/src/frontend-common/frontend-common.vcxproj.filters @@ -16,6 +16,9 @@ + + + @@ -33,6 +36,9 @@ + + + diff --git a/dep/imgui/src/imgui_impl_dx11.cpp b/src/frontend-common/imgui_impl_dx11.cpp similarity index 100% rename from dep/imgui/src/imgui_impl_dx11.cpp rename to src/frontend-common/imgui_impl_dx11.cpp diff --git a/dep/imgui/include/imgui_impl_dx11.h b/src/frontend-common/imgui_impl_dx11.h similarity index 100% rename from dep/imgui/include/imgui_impl_dx11.h rename to src/frontend-common/imgui_impl_dx11.h diff --git a/dep/imgui/src/imgui_impl_opengl3.cpp b/src/frontend-common/imgui_impl_opengl3.cpp similarity index 100% rename from dep/imgui/src/imgui_impl_opengl3.cpp rename to src/frontend-common/imgui_impl_opengl3.cpp diff --git a/dep/imgui/include/imgui_impl_opengl3.h b/src/frontend-common/imgui_impl_opengl3.h similarity index 100% rename from dep/imgui/include/imgui_impl_opengl3.h rename to src/frontend-common/imgui_impl_opengl3.h diff --git a/dep/imgui/src/imgui_impl_vulkan.cpp b/src/frontend-common/imgui_impl_vulkan.cpp similarity index 100% rename from dep/imgui/src/imgui_impl_vulkan.cpp rename to src/frontend-common/imgui_impl_vulkan.cpp diff --git a/dep/imgui/include/imgui_impl_vulkan.h b/src/frontend-common/imgui_impl_vulkan.h similarity index 100% rename from dep/imgui/include/imgui_impl_vulkan.h rename to src/frontend-common/imgui_impl_vulkan.h From 2c36750a0e094ae13a60f81be5f94c685b3ce914 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 5 Sep 2020 17:27:38 +1000 Subject: [PATCH 56/61] VulkanHostDisplay: Fix imgui image textures not rendering --- src/core/gpu_hw_vulkan.cpp | 12 ++-- src/frontend-common/imgui_impl_vulkan.cpp | 79 ++++++++++----------- src/frontend-common/imgui_impl_vulkan.h | 2 +- src/frontend-common/vulkan_host_display.cpp | 4 +- 4 files changed, 44 insertions(+), 53 deletions(-) diff --git a/src/core/gpu_hw_vulkan.cpp b/src/core/gpu_hw_vulkan.cpp index 808abde7c..5f1a31dcc 100644 --- a/src/core/gpu_hw_vulkan.cpp +++ b/src/core/gpu_hw_vulkan.cpp @@ -99,13 +99,6 @@ void GPU_HW_Vulkan::ResetGraphicsAPIState() GPU_HW::ResetGraphicsAPIState(); EndRenderPass(); - - // vram texture is probably going to be displayed now - if (!IsDisplayDisabled()) - { - m_vram_texture.TransitionToLayout(g_vulkan_context->GetCurrentCommandBuffer(), - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - } } void GPU_HW_Vulkan::RestoreGraphicsAPIState() @@ -928,9 +921,12 @@ void GPU_HW_Vulkan::ClearDisplay() void GPU_HW_Vulkan::UpdateDisplay() { GPU_HW::UpdateDisplay(); + EndRenderPass(); if (g_settings.debugging.show_vram) { + m_vram_texture.TransitionToLayout(g_vulkan_context->GetCurrentCommandBuffer(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); m_host_display->SetDisplayTexture(&m_vram_texture, m_vram_texture.GetWidth(), m_vram_texture.GetHeight(), 0, 0, m_vram_texture.GetWidth(), m_vram_texture.GetHeight()); m_host_display->SetDisplayParameters(VRAM_WIDTH, VRAM_HEIGHT, 0, 0, VRAM_WIDTH, VRAM_HEIGHT, @@ -956,6 +952,8 @@ void GPU_HW_Vulkan::UpdateDisplay() (scaled_vram_offset_x + scaled_display_width) <= m_vram_texture.GetWidth() && (scaled_vram_offset_y + scaled_display_height) <= m_vram_texture.GetHeight()) { + m_vram_texture.TransitionToLayout(g_vulkan_context->GetCurrentCommandBuffer(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); m_host_display->SetDisplayTexture(&m_vram_texture, m_vram_texture.GetWidth(), m_vram_texture.GetHeight(), scaled_vram_offset_x, scaled_vram_offset_y, scaled_display_width, scaled_display_height); diff --git a/src/frontend-common/imgui_impl_vulkan.cpp b/src/frontend-common/imgui_impl_vulkan.cpp index 3fb10d235..50c12ef75 100644 --- a/src/frontend-common/imgui_impl_vulkan.cpp +++ b/src/frontend-common/imgui_impl_vulkan.cpp @@ -46,6 +46,7 @@ #include "imgui.h" #include "imgui_impl_vulkan.h" +#include "common/vulkan/texture.h" #include // Reusable buffers used for rendering 1 current in-flight frame, for ImGui_ImplVulkan_RenderDrawData() @@ -76,16 +77,15 @@ static VkDeviceSize g_BufferMemoryAlignment = 256; static VkPipelineCreateFlags g_PipelineCreateFlags = 0x00; static VkDescriptorSetLayout g_DescriptorSetLayout = VK_NULL_HANDLE; static VkPipelineLayout g_PipelineLayout = VK_NULL_HANDLE; -static VkDescriptorSet g_DescriptorSet = VK_NULL_HANDLE; static VkPipeline g_Pipeline = VK_NULL_HANDLE; // Font data static VkSampler g_FontSampler = VK_NULL_HANDLE; static VkDeviceMemory g_FontMemory = VK_NULL_HANDLE; static VkImage g_FontImage = VK_NULL_HANDLE; -static VkImageView g_FontView = VK_NULL_HANDLE; static VkDeviceMemory g_UploadBufferMemory = VK_NULL_HANDLE; static VkBuffer g_UploadBuffer = VK_NULL_HANDLE; +static Vulkan::Texture g_FontTexture; // Render buffers static ImGui_ImplVulkanH_WindowRenderBuffers g_MainWindowRenderBuffers; @@ -269,8 +269,6 @@ static void ImGui_ImplVulkan_SetupRenderState(ImDrawData* draw_data, VkCommandBu // Bind pipeline and descriptor sets: { vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, g_Pipeline); - VkDescriptorSet desc_set[1] = { g_DescriptorSet }; - vkCmdBindDescriptorSets(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, g_PipelineLayout, 0, 1, desc_set, 0, NULL); } // Bind Vertex And Index Buffer: @@ -307,6 +305,33 @@ static void ImGui_ImplVulkan_SetupRenderState(ImDrawData* draw_data, VkCommandBu } } +static void ImGui_ImplVulkan_UpdateAndBindDescriptors(const ImDrawCmd* pcmd, VkCommandBuffer command_buffer) +{ + const Vulkan::Texture* tex = static_cast(pcmd->TextureId); + + VkDescriptorSet desc_set; + VkDescriptorSetAllocateInfo alloc_info = {}; + alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + alloc_info.descriptorPool = g_VulkanInitInfo.DescriptorPool; + alloc_info.descriptorSetCount = 1; + alloc_info.pSetLayouts = &g_DescriptorSetLayout; + VkResult err = vkAllocateDescriptorSets(g_VulkanInitInfo.Device, &alloc_info, &desc_set); + check_vk_result(err); + + VkDescriptorImageInfo desc_image[1] = {}; + desc_image[0].sampler = g_FontSampler; + desc_image[0].imageView = tex->GetView(); + desc_image[0].imageLayout = tex->GetLayout(); + VkWriteDescriptorSet write_desc[1] = {}; + write_desc[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write_desc[0].dstSet = desc_set; + write_desc[0].descriptorCount = 1; + write_desc[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write_desc[0].pImageInfo = desc_image; + vkUpdateDescriptorSets(g_VulkanInitInfo.Device, 1, write_desc, 0, NULL); + vkCmdBindDescriptorSets(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, g_PipelineLayout, 0, 1, &desc_set, 0, NULL); +} + // Render function // (this used to be set in io.RenderDrawListsFn and called by ImGui::Render(), but you can now call this directly from your main loop) void ImGui_ImplVulkan_RenderDrawData(ImDrawData* draw_data, VkCommandBuffer command_buffer) @@ -407,6 +432,8 @@ void ImGui_ImplVulkan_RenderDrawData(ImDrawData* draw_data, VkCommandBuffer comm if (clip_rect.x < fb_width && clip_rect.y < fb_height && clip_rect.z >= 0.0f && clip_rect.w >= 0.0f) { + ImGui_ImplVulkan_UpdateAndBindDescriptors(pcmd, command_buffer); + // Negative offsets are illegal for vkCmdSetScissor if (clip_rect.x < 0.0f) clip_rect.x = 0.0f; @@ -475,31 +502,7 @@ bool ImGui_ImplVulkan_CreateFontsTexture(VkCommandBuffer command_buffer) // Create the Image View: { - VkImageViewCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - info.image = g_FontImage; - info.viewType = VK_IMAGE_VIEW_TYPE_2D; - info.format = VK_FORMAT_R8G8B8A8_UNORM; - info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - info.subresourceRange.levelCount = 1; - info.subresourceRange.layerCount = 1; - err = vkCreateImageView(v->Device, &info, v->Allocator, &g_FontView); - check_vk_result(err); - } - - // Update the Descriptor Set: - { - VkDescriptorImageInfo desc_image[1] = {}; - desc_image[0].sampler = g_FontSampler; - desc_image[0].imageView = g_FontView; - desc_image[0].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - VkWriteDescriptorSet write_desc[1] = {}; - write_desc[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write_desc[0].dstSet = g_DescriptorSet; - write_desc[0].descriptorCount = 1; - write_desc[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - write_desc[0].pImageInfo = desc_image; - vkUpdateDescriptorSets(v->Device, 1, write_desc, 0, NULL); + g_FontTexture.Adopt(g_FontImage, VK_IMAGE_VIEW_TYPE_2D, width, height, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, VK_SAMPLE_COUNT_1_BIT); } // Create the Upload Buffer: @@ -578,7 +581,7 @@ bool ImGui_ImplVulkan_CreateFontsTexture(VkCommandBuffer command_buffer) } // Store our identifier - io.Fonts->TexID = (ImTextureID)(intptr_t)g_FontImage; + io.Fonts->TexID = (ImTextureID)(intptr_t)&g_FontTexture; return true; } @@ -639,17 +642,6 @@ bool ImGui_ImplVulkan_CreateDeviceObjects() check_vk_result(err); } - // Create Descriptor Set: - { - VkDescriptorSetAllocateInfo alloc_info = {}; - alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - alloc_info.descriptorPool = v->DescriptorPool; - alloc_info.descriptorSetCount = 1; - alloc_info.pSetLayouts = &g_DescriptorSetLayout; - err = vkAllocateDescriptorSets(v->Device, &alloc_info, &g_DescriptorSet); - check_vk_result(err); - } - if (!g_PipelineLayout) { // Constants: we are using 'vec2 offset' and 'vec2 scale' instead of a full 3d projection matrix @@ -795,7 +787,7 @@ void ImGui_ImplVulkan_DestroyDeviceObjects() ImGui_ImplVulkanH_DestroyWindowRenderBuffers(v->Device, &g_MainWindowRenderBuffers, v->Allocator); ImGui_ImplVulkan_DestroyFontUploadObjects(); - if (g_FontView) { vkDestroyImageView(v->Device, g_FontView, v->Allocator); g_FontView = VK_NULL_HANDLE; } + g_FontTexture.Destroy(false); if (g_FontImage) { vkDestroyImage(v->Device, g_FontImage, v->Allocator); g_FontImage = VK_NULL_HANDLE; } if (g_FontMemory) { vkFreeMemory(v->Device, g_FontMemory, v->Allocator); g_FontMemory = VK_NULL_HANDLE; } if (g_FontSampler) { vkDestroySampler(v->Device, g_FontSampler, v->Allocator); g_FontSampler = VK_NULL_HANDLE; } @@ -832,8 +824,9 @@ void ImGui_ImplVulkan_Shutdown() ImGui_ImplVulkan_DestroyDeviceObjects(); } -void ImGui_ImplVulkan_NewFrame() +void ImGui_ImplVulkan_NewFrame(VkDescriptorPool DescriptorPool) { + g_VulkanInitInfo.DescriptorPool = DescriptorPool; } void ImGui_ImplVulkan_SetMinImageCount(uint32_t min_image_count) diff --git a/src/frontend-common/imgui_impl_vulkan.h b/src/frontend-common/imgui_impl_vulkan.h index 8ad8180eb..6ee263a9b 100644 --- a/src/frontend-common/imgui_impl_vulkan.h +++ b/src/frontend-common/imgui_impl_vulkan.h @@ -45,7 +45,7 @@ struct ImGui_ImplVulkan_InitInfo // Called by user code IMGUI_IMPL_API bool ImGui_ImplVulkan_Init(ImGui_ImplVulkan_InitInfo* info, VkRenderPass render_pass); IMGUI_IMPL_API void ImGui_ImplVulkan_Shutdown(); -IMGUI_IMPL_API void ImGui_ImplVulkan_NewFrame(); +IMGUI_IMPL_API void ImGui_ImplVulkan_NewFrame(VkDescriptorPool DescriptorPool); IMGUI_IMPL_API void ImGui_ImplVulkan_RenderDrawData(ImDrawData* draw_data, VkCommandBuffer command_buffer); IMGUI_IMPL_API bool ImGui_ImplVulkan_CreateFontsTexture(VkCommandBuffer command_buffer); IMGUI_IMPL_API void ImGui_ImplVulkan_DestroyFontUploadObjects(); diff --git a/src/frontend-common/vulkan_host_display.cpp b/src/frontend-common/vulkan_host_display.cpp index 971c80876..fd6f5dd29 100644 --- a/src/frontend-common/vulkan_host_display.cpp +++ b/src/frontend-common/vulkan_host_display.cpp @@ -466,7 +466,7 @@ bool VulkanHostDisplay::CreateImGuiContext() return false; } - ImGui_ImplVulkan_NewFrame(); + ImGui_ImplVulkan_NewFrame(g_vulkan_context->GetCurrentDescriptorPool()); #endif return true; @@ -532,7 +532,7 @@ bool VulkanHostDisplay::Render() #ifdef WITH_IMGUI if (ImGui::GetCurrentContext()) - ImGui_ImplVulkan_NewFrame(); + ImGui_ImplVulkan_NewFrame(g_vulkan_context->GetCurrentDescriptorPool()); #endif return true; From 976d4bae79db6d15ef533b6ed954d90f04c81ea8 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 6 Sep 2020 17:47:49 +1000 Subject: [PATCH 57/61] TimingEvent: Fix crash when invalid save state loaded --- src/core/timing_event.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/timing_event.cpp b/src/core/timing_event.cpp index 1580aa188..8197eff80 100644 --- a/src/core/timing_event.cpp +++ b/src/core/timing_event.cpp @@ -207,7 +207,8 @@ static void RemoveActiveEvent(TimingEvent* event) else { s_active_events_head = event->next; - UpdateCPUDowncount(); + if (s_active_events_head) + UpdateCPUDowncount(); } event->prev = nullptr; From 4f8fd049d0f5f304aa50ed26820dce58f801f151 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 6 Sep 2020 17:48:01 +1000 Subject: [PATCH 58/61] SDL: Fix crash on shutdown when save state selector used --- src/duckstation-sdl/sdl_host_interface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp index 23d49de29..d1ac582f6 100644 --- a/src/duckstation-sdl/sdl_host_interface.cpp +++ b/src/duckstation-sdl/sdl_host_interface.cpp @@ -420,6 +420,8 @@ void SDLHostInterface::Shutdown() { DestroySystem(); + CommonHostInterface::Shutdown(); + if (m_display) { DestroyDisplay(); @@ -428,8 +430,6 @@ void SDLHostInterface::Shutdown() if (m_window) DestroySDLWindow(); - - CommonHostInterface::Shutdown(); } std::string SDLHostInterface::GetStringSettingValue(const char* section, const char* key, From 75ad685ae8582f0ac60bcbd202efeb8ef436b5d7 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 6 Sep 2020 17:48:58 +1000 Subject: [PATCH 59/61] CommonHostInterface: Fix incorrect version error string in save state selector --- src/frontend-common/common_host_interface.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 74f834644..6d7b197d7 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -1807,8 +1807,7 @@ CommonHostInterface::GetExtendedSaveStateInfo(const char* game_code, s32 slot) if (header.version != SAVE_STATE_VERSION) { - ssi.title = StringUtil::StdStringFromFormat("Invalid version %u (expected %u)", header.version, header.magic, - SAVE_STATE_VERSION); + ssi.title = StringUtil::StdStringFromFormat("Invalid version %u (expected %u)", header.version, SAVE_STATE_VERSION); return ssi; } From 7286dbab51f6e3bfa50ec74e700b50dd61375603 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 6 Sep 2020 17:55:08 +1000 Subject: [PATCH 60/61] Qt: Fix sorting game list by title being case sensitive --- src/duckstation-qt/gamelistmodel.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/duckstation-qt/gamelistmodel.cpp b/src/duckstation-qt/gamelistmodel.cpp index 7946dbeba..62dc163ba 100644 --- a/src/duckstation-qt/gamelistmodel.cpp +++ b/src/duckstation-qt/gamelistmodel.cpp @@ -188,7 +188,8 @@ bool GameListModel::titlesLessThan(int left_row, int right_row, bool ascending) const GameListEntry& left = m_game_list->GetEntries().at(left_row); const GameListEntry& right = m_game_list->GetEntries().at(right_row); - return ascending ? (left.title < right.title) : (right.title < left.title); + return ascending ? (StringUtil::Strcasecmp(left.title.c_str(), right.title.c_str()) < 0) : + (StringUtil::Strcasecmp(right.title.c_str(), left.title.c_str()) > 0); } bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& right_index, int column, @@ -222,15 +223,13 @@ bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& r { if (left.code == right.code) return titlesLessThan(left_row, right_row, ascending); - return ascending ? (left.code < right.code) : (right.code > left.code); + return ascending ? (StringUtil::Strcasecmp(left.code.c_str(), right.code.c_str()) < 0) : + (StringUtil::Strcasecmp(right.code.c_str(), left.code.c_str()) > 0); } case Column_Title: { - if (left.title == right.title) - return titlesLessThan(left_row, right_row, ascending); - - return ascending ? (left.title < right.title) : (right.title > left.title); + return titlesLessThan(left_row, right_row, ascending); } case Column_FileTitle: @@ -240,7 +239,9 @@ bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& r if (file_title_left == file_title_right) return titlesLessThan(left_row, right_row, ascending); - return ascending ? (file_title_left < file_title_right) : (file_title_right > file_title_left); + const std::size_t smallest = std::min(file_title_left.size(), file_title_right.size()); + return ascending ? (StringUtil::Strncasecmp(file_title_left.data(), file_title_right.data(), smallest) < 0) : + (StringUtil::Strncasecmp(file_title_right.data(), file_title_left.data(), smallest) > 0); } case Column_Region: From d5d79e952cf4fa6d8329151cb870372f1e0883e7 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 6 Sep 2020 18:27:50 +1000 Subject: [PATCH 61/61] Fix CMake build --- src/frontend-common/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index 91585ff6a..710f81141 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -16,18 +16,18 @@ if(WIN32) endif() if(NOT BUILD_LIBRETRO_CORE) - target_sources(imgui PRIVATE + target_sources(frontend-common PRIVATE imgui_impl_opengl3.h imgui_impl_opengl3.cpp ) - target_sources(imgui PRIVATE + target_sources(frontend-common PRIVATE imgui_impl_vulkan.h imgui_impl_vulkan.cpp ) if(WIN32) - target_sources(imgui PRIVATE + target_sources(frontend-common PRIVATE imgui_impl_dx11.h imgui_impl_dx11.cpp )