Fix SMO synchronization (#300)

This commit is contained in:
Bruno 2026-09-27 13:22:05 +02:00 committed by GitHub
parent 7a2a68f7ee
commit 8855290cfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 148 additions and 30 deletions

View File

@ -36,7 +36,7 @@
#StatusBar #volume,
#StatusBar #renderer,
#StatusBar #dockedMode,
#StatusBar #syncMemoryOperations,
#StatusBar #gpuCommandSynchronization,
#StatusBar #volumePopupBtn
{
border: 1px solid transparent;
@ -48,7 +48,7 @@
#StatusBar #volume:hover,
#StatusBar #renderer:hover,
#StatusBar #dockedMode:hover,
#StatusBar #syncMemoryOperations:hover,
#StatusBar #gpuCommandSynchronization:hover,
#StatusBar #volumePopupBtn:hover
{
border: 1px solid color(color-statusbar-hover-border);

View File

@ -43,7 +43,7 @@
<div id="StatusBar" >
<span id="dockedMode" />
<span id="renderer" />
<span id="syncMemoryOperations" tooltip="Allow Sync Memory Operations to be toggled at runtime for performance testing." />
<span id="gpuCommandSynchronization" tooltip="Toggle guest-requested GPU command synchronization at runtime." />
<span id="volumeAnchor">
<span id="volume" />
<span id="volumePopupBtn"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M104.704 685.248a64 64 0 0 0 90.496 0l316.8-316.8 316.8 316.8a64 64 0 0 0 90.496-90.496L557.248 232.704a64 64 0 0 0-90.496 0L104.704 594.752a64 64 0 0 0 0 90.496"/></svg></span>

View File

@ -71,8 +71,8 @@
<div class="right"><ComboBox id="DMAAccuracy" tooltip="Controls the DMA read mode.\nUnsafe is faster, while Safe is more stable and can fix issues in some games.\nDefault follows the GPU Accuracy setting."/></div>
</div>
<div class="form-row checkbox-row">
<div class="left"><label for="SyncMemoryOperations">Sync Memory Operations</label></div>
<div class="right"><input type="checkbox" id="SyncMemoryOperations" tooltip="Ensures data consistency between compute and memory operations.\nThis option fixes issues in games, but may degrade performance.\nUnreal Engine 4 games often see the most significant changes thereof."></div>
<div class="left"><label for="GpuCommandSynchronization">GPU Command Synchronization</label></div>
<div class="right"><input type="checkbox" id="GpuCommandSynchronization" tooltip="Preserves guest-requested GPFIFO command ordering.\nClean command buffers avoid unnecessary GPU stalls, while command buffers that may depend on GPU-written memory are synchronized conservatively.\nDisabling this option ignores these guest synchronization requests and may cause graphical issues, hangs, or crashes."></div>
</div>
<div class="form-row">
<div class="left"><label for="AnisotropicFiltering">Anisotropic Filtering:</label></div>

View File

@ -16,7 +16,7 @@ namespace
{
// Configuration storage stays on the settings thread; the GPU reads these snapshots.
std::atomic<int32_t> runtime_dma_accuracy{0};
std::atomic<bool> runtime_sync_memory_operations{true};
std::atomic<bool> runtime_gpu_command_synchronization{true};
}
namespace Settings
@ -54,9 +54,9 @@ bool UseSafeDMAReads()
return accuracy == 0 ? IsGPULevelHigh() : accuracy == 2;
}
bool IsSyncMemoryOperationsEnabled()
bool IsGpuCommandSynchronizationEnabled()
{
return runtime_sync_memory_operations.load(std::memory_order_relaxed);
return runtime_gpu_command_synchronization.load(std::memory_order_relaxed);
}
void TranslateResolutionInfo(ResolutionSetup setup, ResolutionScalingInfo & info)
@ -243,7 +243,7 @@ static VideoSetting settings[] = {
{NXVideoSetting::AnisotropicFiltering, "video", "anisotropic_filtering", &videoSettings.max_anisotropy, AnisotropyMode::Automatic},
#endif
{NXVideoSetting::DMAAccuracy, "video", "dma_accuracy", &videoSettings.dma_accuracy, 0, 0, 2},
{NXVideoSetting::SyncMemoryOperations, "video", "sync_memory_operations", &videoSettings.sync_memory_operations, true},
{NXVideoSetting::GpuCommandSynchronization, "video", "gpu_command_synchronization", &videoSettings.gpu_command_synchronization, true},
{NXVideoSetting::VSyncMode, "video", "vsync_mode", &videoSettings.vsync_mode, VSyncMode::Fifo},
{NXVideoSetting::NvdecEmulation, "video", "nvdec_emulation", &videoSettings.nvdec_emulation, NvdecEmulation::Gpu},
#ifdef _WIN32
@ -345,9 +345,9 @@ void VideoSettingChanged(const char * setting, void * /*userData*/)
runtime_dma_accuracy.store(videoSettings.dma_accuracy, std::memory_order_relaxed);
return;
}
if (strcmp(setting, NXVideoSetting::SyncMemoryOperations) == 0)
if (strcmp(setting, NXVideoSetting::GpuCommandSynchronization) == 0)
{
runtime_sync_memory_operations.store(videoSettings.sync_memory_operations, std::memory_order_relaxed);
runtime_gpu_command_synchronization.store(videoSettings.gpu_command_synchronization, std::memory_order_relaxed);
return;
}
}
@ -618,7 +618,7 @@ void SetupVideoSetting(void)
g_settings->RegisterCallback(videoSetting.identifier, VideoSettingChanged, nullptr);
}
runtime_dma_accuracy.store(videoSettings.dma_accuracy, std::memory_order_relaxed);
runtime_sync_memory_operations.store(videoSettings.sync_memory_operations, std::memory_order_relaxed);
runtime_gpu_command_synchronization.store(videoSettings.gpu_command_synchronization, std::memory_order_relaxed);
Settings::UpdateRescalingInfo();
Settings::UpdateGPUAccuracy();
}

View File

@ -41,7 +41,7 @@ bool IsGPULevelHigh();
bool IsDMALevelDefault();
bool IsDMALevelSafe();
bool UseSafeDMAReads();
bool IsSyncMemoryOperationsEnabled();
bool IsGpuCommandSynchronizationEnabled();
void TranslateResolutionInfo(ResolutionSetup setup, ResolutionScalingInfo & info);
void UpdateRescalingInfo();
@ -80,7 +80,7 @@ struct VideoSettings
AnisotropyMode max_anisotropy;
bool async_presentation;
bool use_reactive_flushing;
bool sync_memory_operations;
bool gpu_command_synchronization;
bool use_asynchronous_shaders;
bool use_fast_gpu_time;

View File

@ -19,7 +19,7 @@ namespace NXVideoSetting
constexpr const char * EnableAsynchronousPresentation = "nxvideo:EnableAsynchronousPresentation";
constexpr const char * ForceMaximumClocks = "nxvideo:ForceMaximumClocks";
constexpr const char * EnableReactiveFlushing = "nxvideo:EnableReactiveFlushing";
constexpr const char * SyncMemoryOperations = "nxvideo:SyncMemoryOperations";
constexpr const char * GpuCommandSynchronization = "nxvideo:GpuCommandSynchronization";
constexpr const char * UseAsynchronousShaderBuilding = "nxvideo:UseAsynchronousShaderBuilding";
constexpr const char * FastGPUTime = "nxvideo:FastGPUTime";
constexpr const char * UseVulkanPipelineCache = "nxvideo:UseVulkanPipelineCache";

View File

@ -287,7 +287,7 @@ SciterMainWindow::SciterMainWindow(ISciterUI & sciterUI, const char * windowTitl
settings.RegisterCallback(NXOsSetting::DockedMode, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXUISetting::Hotkeys, SciterMainWindow::HotKeysChanged, this);
settings.RegisterCallback(NXVideoSetting::ResolutionUpFactor, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXVideoSetting::SyncMemoryOperations, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXVideoSetting::GpuCommandSynchronization, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXUISetting::HideMouseOnInactivity, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXUISetting::EnableDiscordPresence, SciterMainWindow::SettingChanged, this);
@ -384,7 +384,7 @@ SciterMainWindow::~SciterMainWindow()
settings.UnregisterCallback(NXOsSetting::DockedMode, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXUISetting::Hotkeys, SciterMainWindow::HotKeysChanged, this);
settings.UnregisterCallback(NXVideoSetting::ResolutionUpFactor, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXVideoSetting::SyncMemoryOperations, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXVideoSetting::GpuCommandSynchronization, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXUISetting::HideMouseOnInactivity, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXUISetting::EnableDiscordPresence, SciterMainWindow::SettingChanged, this);
@ -656,7 +656,7 @@ bool SciterMainWindow::Show()
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("dockedMode"), IID_ICLICKSINK, (IClickSink *)this);
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("renderer"), IID_ICLICKSINK, (IClickSink *)this);
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("syncMemoryOperations"), IID_ICLICKSINK, (IClickSink *)this);
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("gpuCommandSynchronization"), IID_ICLICKSINK, (IClickSink *)this);
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("volume"), IID_ICLICKSINK, (IClickSink *)this);
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("volumePopupBtn"), IID_ICLICKSINK, (IClickSink *)this);
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("audioVolume"), IID_ISTATECHANGESINK, (IStateChangeSink *)this);
@ -847,12 +847,12 @@ void SciterMainWindow::UpdateStatusWidgets()
renderer.SetHTML((const uint8_t *)text.c_str(), text.size());
}
SciterElement syncMemoryOperations(m_rootElement.GetElementByID("syncMemoryOperations"));
if (syncMemoryOperations.IsValid())
SciterElement gpuCommandSynchronization(m_rootElement.GetElementByID("gpuCommandSynchronization"));
if (gpuCommandSynchronization.IsValid())
{
const bool enabled = settings.GetBool(NXVideoSetting::SyncMemoryOperations);
stdstr_f text("SMO: %s", enabled ? "ON" : "OFF");
syncMemoryOperations.SetHTML((const uint8_t *)text.c_str(), text.size());
const bool enabled = settings.GetBool(NXVideoSetting::GpuCommandSynchronization);
stdstr_f text("GPU Sync: %s", enabled ? "ON" : "OFF");
gpuCommandSynchronization.SetHTML((const uint8_t *)text.c_str(), text.size());
}
SciterElement volume(m_rootElement.GetElementByID("volume"));
@ -2400,11 +2400,11 @@ bool SciterMainWindow::OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, ui
m_modules.FlushSettings();
}
}
else if (source == rootElement.GetElementByID("syncMemoryOperations"))
else if (source == rootElement.GetElementByID("gpuCommandSynchronization"))
{
SettingsStore & settings = SettingsStore::GetInstance();
const bool enabled = settings.GetBool(NXVideoSetting::SyncMemoryOperations);
settings.SetBool(NXVideoSetting::SyncMemoryOperations, !enabled);
const bool enabled = settings.GetBool(NXVideoSetting::GpuCommandSynchronization);
settings.SetBool(NXVideoSetting::GpuCommandSynchronization, !enabled);
UpdateStatusWidgets();
}
else if (source == rootElement.GetElementByID("volume"))
@ -2464,7 +2464,7 @@ void SciterMainWindow::SettingChanged(const char * setting, void * userData)
{
impl->m_resolutionUpFactor = SettingsStore::GetInstance().GetFloat(NXVideoSetting::ResolutionUpFactor);
}
else if (strcmp(setting, NXVideoSetting::SyncMemoryOperations) == 0)
else if (strcmp(setting, NXVideoSetting::GpuCommandSynchronization) == 0)
{
impl->UpdateStatusWidgets();
}

View File

@ -28,7 +28,7 @@ namespace
static ConfigSetting advancedSettings[] = {
ConfigSetting(ConfigSetting::ComboBox, "AccuracyLevel", true, SystemConfig::TranslationType::GpuAccuracy, NXVideoSetting::AccuracyLevel),
ConfigSetting(ConfigSetting::ComboBox, "DMAAccuracy", true, SystemConfig::TranslationType::DmaAccuracy, NXVideoSetting::DMAAccuracy),
ConfigSetting(ConfigSetting::CheckBox, "SyncMemoryOperations", true, NXVideoSetting::SyncMemoryOperations),
ConfigSetting(ConfigSetting::CheckBox, "GpuCommandSynchronization", true, NXVideoSetting::GpuCommandSynchronization),
ConfigSetting(ConfigSetting::ComboBox, "AnisotropicFiltering", false, SystemConfig::TranslationType::AnisotropyMode, NXVideoSetting::AnisotropicFiltering),
ConfigSetting(ConfigSetting::ComboBox, "ASTCRecompressionMethod", false, SystemConfig::TranslationType::AstcRecompression, NXVideoSetting::ASTCRecompressionMethod),
ConfigSetting(ConfigSetting::ComboBox, "VRAMUsageMode", false, SystemConfig::TranslationType::VramUsageMode, NXVideoSetting::VRAMUsageMode),

View File

@ -41,6 +41,16 @@ void DmaPusher::DispatchCalls()
break;
}
}
if (memory_operation_sync_state == MemoryOperationSyncState::Armed)
{
WaitForArmedMemoryOperationSync();
}
else
{
memory_operation_sync_state = MemoryOperationSyncState::None;
}
gpu.FlushCommands();
gpu.OnCommandListEnd();
}
@ -75,9 +85,27 @@ bool DmaPusher::Step()
dma_state.dma_get = header.Address();
// SYNC_WAIT applies to the current GP entry, including zero-length entries.
if (header.sync && Settings::IsSyncMemoryOperationsEnabled())
if (header.sync)
{
SynchronizeMemoryOperations();
switch (memory_operation_sync_state)
{
case MemoryOperationSyncState::Skip:
memory_operation_sync_state = MemoryOperationSyncState::None;
break;
case MemoryOperationSyncState::Armed:
WaitForArmedMemoryOperationSync();
break;
case MemoryOperationSyncState::None:
if (Settings::IsGpuCommandSynchronizationEnabled())
{
SynchronizeMemoryOperations();
}
break;
}
}
else
{
ASSERT(memory_operation_sync_state == MemoryOperationSyncState::None);
}
if (header.size > 0 && Settings::IsGPULevelHigh() &&
@ -104,6 +132,26 @@ bool DmaPusher::Step()
}
}
// Prepare a following SYNC_WAIT at the current GP-entry boundary. Clean pushbuffers can be
// fetched directly because DmaPusher already processes GP entries in order.
const std::size_t next_entry_index = dma_pushbuffer_subindex + 1;
if (next_entry_index < command_list_size && Settings::IsGpuCommandSynchronizationEnabled())
{
const CommandListHeader & next_header = command_list.command_lists[next_entry_index];
if (next_header.sync)
{
ASSERT(memory_operation_sync_state == MemoryOperationSyncState::None);
if (CanSkipCleanMemoryOperationSync(next_header))
{
memory_operation_sync_state = MemoryOperationSyncState::Skip;
}
else
{
ArmMemoryOperationSync();
}
}
}
if (++dma_pushbuffer_subindex >= command_list_size)
{
dma_pushbuffer.pop();
@ -119,6 +167,38 @@ void DmaPusher::SynchronizeMemoryOperations()
rasterizer->WaitForFence();
}
void DmaPusher::ArmMemoryOperationSync()
{
ASSERT(rasterizer != nullptr);
ASSERT(memory_operation_sync_state == MemoryOperationSyncState::None);
memory_operation_sync_ready.store(false, std::memory_order_relaxed);
memory_operation_sync_state = MemoryOperationSyncState::Armed;
rasterizer->SignalMemoryOperationFence([this]() {
memory_operation_sync_ready.store(true, std::memory_order_release);
memory_operation_sync_ready.notify_one();
});
}
void DmaPusher::WaitForArmedMemoryOperationSync()
{
ASSERT(memory_operation_sync_state == MemoryOperationSyncState::Armed);
memory_operation_sync_ready.wait(false, std::memory_order_acquire);
memory_operation_sync_state = MemoryOperationSyncState::None;
}
bool DmaPusher::CanSkipCleanMemoryOperationSync(const CommandListHeader & header) const
{
// The actual fetch must still resolve writes that can appear after this look-ahead probe.
if (!Settings::UseSafeDMAReads() || header.size == 0 || header.conditional_fetch)
{
return false;
}
return !memory_manager.IsMemoryDirty(header.Address(), header.size * sizeof(u32));
}
void DmaPusher::ProcessCommands(std::span<const CommandHeader> commands)
{
for (std::size_t index = 0; index < commands.size();)

View File

@ -4,6 +4,7 @@
#pragma once
#include <array>
#include <atomic>
#include <span>
#include <vector>
#include <boost/container/small_vector.hpp>
@ -147,8 +148,17 @@ public:
private:
static constexpr u32 non_puller_methods = 0x40;
static constexpr u32 max_subchannels = 8;
enum class MemoryOperationSyncState : u8 {
None,
Skip,
Armed,
};
bool Step();
void SynchronizeMemoryOperations();
void ArmMemoryOperationSync();
void WaitForArmedMemoryOperationSync();
bool CanSkipCleanMemoryOperationSync(const CommandListHeader& header) const;
void ProcessCommands(std::span<const CommandHeader> commands);
void SetState(const CommandHeader& command_header);
@ -175,6 +185,8 @@ private:
DmaState dma_state{};
bool dma_increment_once{};
MemoryOperationSyncState memory_operation_sync_state{MemoryOperationSyncState::None};
std::atomic<bool> memory_operation_sync_ready{};
const bool ib_enable{true}; ///< IB mode enabled

View File

@ -76,6 +76,15 @@ public:
(void)SubmitFence(std::move(func), force_delay, false, nullptr);
}
void SignalMemoryOperationFence(std::function<void()>&& func) {
const bool completed_inline = SubmitFence(std::move(func), true, true, nullptr);
if constexpr (!can_async_check) {
if (!completed_inline) {
TryReleasePendingFences<true>();
}
}
}
void SignalSyncPoint(u32 value) {
syncpoint_manager.IncrementGuest(value);
std::function<void()> func([this, value] { syncpoint_manager.IncrementHost(value); });

View File

@ -68,6 +68,9 @@ public:
/// Signal a GPU based semaphore as a fence
virtual void SignalFence(std::function<void()>&& func) = 0;
/// Arm an explicit memory-operation synchronization fence without blocking the DMA pusher.
virtual void SignalMemoryOperationFence(std::function<void()>&& func) = 0;
/// Signal a fence and wait for its deferred synchronization operations to complete.
virtual void WaitForFence() = 0;

View File

@ -66,6 +66,9 @@ void RasterizerNull::ModifyGPUMemory(size_t as_id, GPUVAddr addr, u64 size) {}
void RasterizerNull::SignalFence(std::function<void()>&& func) {
func();
}
void RasterizerNull::SignalMemoryOperationFence(std::function<void()>&& func) {
func();
}
void RasterizerNull::WaitForFence() {}
void RasterizerNull::SyncOperation(std::function<void()>&& func) {
func();

View File

@ -60,6 +60,7 @@ public:
void UnmapMemory(DAddr addr, u64 size) override;
void ModifyGPUMemory(size_t as_id, GPUVAddr addr, u64 size) override;
void SignalFence(std::function<void()>&& func) override;
void SignalMemoryOperationFence(std::function<void()>&& func) override;
void WaitForFence() override;
void SyncOperation(std::function<void()>&& func) override;
void SignalSyncPoint(u32 value) override;

View File

@ -615,6 +615,10 @@ void RasterizerOpenGL::SignalFence(std::function<void()>&& func) {
fence_manager.SignalFence(std::move(func));
}
void RasterizerOpenGL::SignalMemoryOperationFence(std::function<void()>&& func) {
fence_manager.SignalMemoryOperationFence(std::move(func));
}
void RasterizerOpenGL::WaitForFence() {
fence_manager.WaitForFence();
}

View File

@ -105,6 +105,7 @@ public:
void UnmapMemory(DAddr addr, u64 size) override;
void ModifyGPUMemory(size_t as_id, GPUVAddr addr, u64 size) override;
void SignalFence(std::function<void()>&& func) override;
void SignalMemoryOperationFence(std::function<void()>&& func) override;
void WaitForFence() override;
void SyncOperation(std::function<void()>&& func) override;
void SignalSyncPoint(u32 value) override;

View File

@ -685,6 +685,10 @@ void RasterizerVulkan::SignalFence(std::function<void()>&& func) {
fence_manager.SignalFence(std::move(func));
}
void RasterizerVulkan::SignalMemoryOperationFence(std::function<void()>&& func) {
fence_manager.SignalMemoryOperationFence(std::move(func));
}
void RasterizerVulkan::WaitForFence() {
fence_manager.WaitForFence();
}

View File

@ -107,6 +107,7 @@ public:
void UnmapMemory(DAddr addr, u64 size) override;
void ModifyGPUMemory(size_t as_id, GPUVAddr addr, u64 size) override;
void SignalFence(std::function<void()>&& func) override;
void SignalMemoryOperationFence(std::function<void()>&& func) override;
void WaitForFence() override;
void SyncOperation(std::function<void()>&& func) override;
void SignalSyncPoint(u32 value) override;