Common: Make HookableEvent use non-static data.

This commit is contained in:
Jordan Woyak 2025-05-03 02:48:44 -05:00
parent d2db9d9590
commit 7d75c3e86d
22 changed files with 153 additions and 110 deletions

View File

@ -19,36 +19,38 @@ struct HookBase
{
virtual ~HookBase() = default;
protected:
HookBase() = default;
// This shouldn't be copied. And since we always wrap it in unique_ptr, no need to move it either
HookBase(const HookBase&) = delete;
HookBase(HookBase&&) = delete;
HookBase& operator=(const HookBase&) = delete;
HookBase& operator=(HookBase&&) = delete;
protected:
HookBase() = default;
};
// EventHook is a handle a registered listener holds.
// When the handle is destroyed, the HookableEvent will automatically remove the listener.
// If the handle outlives the HookableEvent, the link will be properly disconnected.
using EventHook = std::unique_ptr<HookBase>;
// A hookable event system.
//
// Define Events in a header as:
// Define Events as:
//
// using MyLoveyEvent = HookableEvent<"My lovely event", std::string, u32>;
// HookableEvent<"My lovely event", std::string, u32> my_lovey_event;
//
// Register listeners anywhere you need them as:
// EventHook myHook = MyLoveyEvent::Register([](std::string foo, u32 bar) {
// EventHook myHook = my_lovey_event.Register([](std::string foo, u32 bar) {
// fmt::print("I've been triggered with {} and {}", foo, bar)
// }, "NameOfHook");
//
// The hook will be automatically unregistered when the EventHook object goes out of scope.
// Trigger events by calling Trigger as:
//
// MyLoveyEvent::Trigger("Hello world", 42);
// my_lovey_event.Trigger("Hello world", 42);
//
template <StringLiteral EventName, typename... CallbackArgs>
class HookableEvent
{
@ -56,60 +58,68 @@ public:
using CallbackType = std::function<void(CallbackArgs...)>;
private:
struct HookImpl final : public HookBase
struct Storage;
struct HookImpl final : HookBase
{
~HookImpl() override { HookableEvent::Remove(this); }
HookImpl(CallbackType callback, std::string name)
: m_fn(std::move(callback)), m_name(std::move(name))
HookImpl(std::weak_ptr<Storage> storage, CallbackType func, std::string name)
: m_storage{std::move(storage)}, m_function{std::move(func)}, m_name{std::move(name)}
{
}
CallbackType m_fn;
std::string m_name;
~HookImpl() override
{
const auto storage = m_storage.lock();
if (storage == nullptr)
{
DEBUG_LOG_FMT(COMMON, "Handler {} outlived event hook {}", m_name, EventName.value);
return;
}
DEBUG_LOG_FMT(COMMON, "Removing {} handler at {} event hook", m_name, EventName.value);
storage->RemoveHook(this);
}
std::weak_ptr<Storage> m_storage;
const CallbackType m_function;
const std::string m_name;
};
struct Storage
{
std::recursive_mutex m_mutex;
void RemoveHook(HookImpl* handle)
{
std::lock_guard lock(m_mutex);
std::erase(m_listeners, handle);
}
std::mutex m_mutex;
std::vector<HookImpl*> m_listeners;
};
// We use the "Construct On First Use" idiom to avoid the static initialization order fiasco.
// https://isocpp.org/wiki/faq/ctors#static-init-order
static Storage& GetStorage()
{
static Storage storage;
return storage;
}
static void Remove(HookImpl* handle)
{
auto& storage = GetStorage();
std::lock_guard lock(storage.m_mutex);
std::erase(storage.m_listeners, handle);
}
public:
// Returns a handle that will unregister the listener when destroyed.
[[nodiscard]] static EventHook Register(CallbackType callback, std::string name)
// Note: Attempting to add/remove hooks of the event within the callback itself will NOT end well.
[[nodiscard]] EventHook Register(CallbackType callback, std::string name)
{
auto& storage = GetStorage();
std::lock_guard lock(storage.m_mutex);
DEBUG_LOG_FMT(COMMON, "Registering {} handler at {} event hook", name, EventName.value);
auto handle = std::make_unique<HookImpl>(std::move(callback), std::move(name));
storage.m_listeners.push_back(handle.get());
auto handle = std::make_unique<HookImpl>(m_storage, std::move(callback), std::move(name));
std::lock_guard lock(m_storage->m_mutex);
m_storage->m_listeners.push_back(handle.get());
return handle;
}
static void Trigger(const CallbackArgs&... args)
void Trigger(const CallbackArgs&... args)
{
auto& storage = GetStorage();
std::lock_guard lock(storage.m_mutex);
for (const auto& handle : storage.m_listeners)
handle->m_fn(args...);
std::lock_guard lock(m_storage->m_mutex);
for (auto* const handle : m_storage->m_listeners)
handle->m_function(args...);
}
private:
// shared_ptr storage allows hooks to forget their connection if they outlive the event itself.
std::shared_ptr<Storage> m_storage{std::make_shared<Storage>()};
};
} // namespace Common

View File

@ -112,7 +112,7 @@ static std::atomic<State> s_state = State::Uninitialized;
static std::unique_ptr<MemoryWatcher> s_memory_watcher;
#endif
void Callback_FramePresented(const PresentInfo& present_info);
static void Callback_FramePresented(const PresentInfo& present_info);
struct HostJob
{
@ -130,8 +130,8 @@ static thread_local bool tls_is_host_thread = false;
static void EmuThread(Core::System& system, std::unique_ptr<BootParameters> boot,
WindowSystemInfo wsi);
static Common::EventHook s_frame_presented =
AfterPresentEvent::Register(&Core::Callback_FramePresented, "Core Frame Presented");
static Common::EventHook s_frame_presented = GetVideoEvents().after_present_event.Register(
&Core::Callback_FramePresented, "Core Frame Presented");
bool GetIsThrottlerTempDisabled()
{

View File

@ -255,7 +255,7 @@ void FifoRecorder::StartRecording(s32 numFrames, CallbackFunc finishedCb)
m_RequestedRecordingEnd = false;
m_FinishedCb = finishedCb;
m_end_of_frame_event = AfterFrameEvent::Register(
m_end_of_frame_event = m_system.GetVideoEvents().after_frame_event.Register(
[this](const Core::System& system) {
const bool was_recording = OpcodeDecoder::g_record_fifo_data;
OpcodeDecoder::g_record_fifo_data = IsRecording();

View File

@ -863,7 +863,7 @@ void VideoInterfaceManager::EndField(FieldType field, u64 ticks)
m_system.GetCoreTiming().Throttle(ticks);
g_perf_metrics.CountVBlank();
VIEndFieldEvent::Trigger();
m_system.GetVideoEvents().vi_end_field_event.Trigger();
Core::OnFrameEnd(m_system);
}

View File

@ -59,6 +59,9 @@ struct System::Impl
{
}
// Built first since other constructors may register hooks right away.
VideoEvents m_video_events;
std::unique_ptr<SoundStream> m_sound_stream;
bool m_sound_stream_running = false;
bool m_audio_dump_started = false;
@ -339,4 +342,14 @@ VideoCommon::CustomAssetLoader& System::GetCustomAssetLoader() const
{
return m_impl->m_custom_asset_loader;
}
VideoEvents& System::GetVideoEvents() const
{
return m_impl->m_video_events;
}
} // namespace Core
VideoEvents& GetVideoEvents()
{
return Core::System::GetInstance().GetVideoEvents();
}

View File

@ -4,6 +4,7 @@
#pragma once
#include <memory>
#include "VideoCommon/VideoEvents.h"
class GeometryShaderManager;
class Interpreter;
@ -198,6 +199,7 @@ public:
XFStateManager& GetXFStateManager() const;
VideoInterface::VideoInterfaceManager& GetVideoInterface() const;
VideoCommon::CustomAssetLoader& GetCustomAssetLoader() const;
VideoEvents& GetVideoEvents() const;
private:
System();

View File

@ -12,6 +12,7 @@
#include "Core/CheatSearch.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/System.h"
#include "UICommon/GameFile.h"
@ -79,7 +80,8 @@ void CheatsManager::UpdateAllCheatSearchWidgetCurrentValues()
void CheatsManager::RegisterAfterFrameEventCallback()
{
m_VI_end_field_event = VIEndFieldEvent::Register([this] { OnFrameEnd(); }, "CheatsManager");
m_VI_end_field_event = m_system.GetVideoEvents().vi_end_field_event.Register(
[this] { OnFrameEnd(); }, "CheatsManager");
}
void CheatsManager::RemoveAfterFrameEventCallback()

View File

@ -386,7 +386,8 @@ void MemoryWidget::hideEvent(QHideEvent* event)
void MemoryWidget::RegisterAfterFrameEventCallback()
{
m_vi_end_field_event = VIEndFieldEvent::Register([this] { AutoUpdateTable(); }, "MemoryWidget");
m_vi_end_field_event = m_system.GetVideoEvents().vi_end_field_event.Register(
[this] { AutoUpdateTable(); }, "MemoryWidget");
}
void MemoryWidget::RemoveAfterFrameEventCallback()

View File

@ -17,8 +17,8 @@ std::unique_ptr<AbstractGfx> g_gfx;
AbstractGfx::AbstractGfx()
{
m_config_changed =
ConfigChangedEvent::Register([this](u32 bits) { OnConfigChanged(bits); }, "AbstractGfx");
m_config_changed = GetVideoEvents().config_changed_event.Register(
[this](u32 bits) { OnConfigChanged(bits); }, "AbstractGfx");
}
bool AbstractGfx::IsHeadless() const

View File

@ -339,6 +339,8 @@ static void BPWritten(PixelShaderManager& pixel_shader_manager, XFStateManager&
false, false, yScale, s_gammaLUT[PE_copy.gamma], bpmem.triggerEFBCopy.clamp_top,
bpmem.triggerEFBCopy.clamp_bottom, bpmem.copyfilter.GetCoefficients());
auto& system = Core::System::GetInstance();
// This is as closest as we have to an "end of the frame"
// It works 99% of the time.
// But sometimes games want to render an XFB larger than the EFB's 640x528 pixel resolution
@ -346,7 +348,7 @@ static void BPWritten(PixelShaderManager& pixel_shader_manager, XFStateManager&
// render multiple sub-frames and arrange the XFB copies in next to each-other in main memory
// so they form a single completed XFB.
// See https://dolphin-emu.org/blog/2017/11/19/hybridxfb/ for examples and more detail.
AfterFrameEvent::Trigger(Core::System::GetInstance());
system.GetVideoEvents().after_frame_event.Trigger(Core::System::GetInstance());
// Note: Theoretically, in the future we could track the VI configuration and try to detect
// when an XFB is the last XFB copy of a frame. Not only would we get a clean "end of
@ -354,7 +356,6 @@ static void BPWritten(PixelShaderManager& pixel_shader_manager, XFStateManager&
// Might also clean up some issues with games doing XFB copies they don't intend to
// display.
auto& system = Core::System::GetInstance();
if (g_ActiveConfig.bImmediateXFB)
{
// TODO: GetTicks is not sane from the GPU thread.

View File

@ -29,8 +29,8 @@ static bool DumpFrameToPNG(const FrameData& frame, const std::string& file_name)
FrameDumper::FrameDumper()
{
m_frame_end_handle =
AfterFrameEvent::Register([this](Core::System&) { FlushFrameDump(); }, "FrameDumper");
m_frame_end_handle = GetVideoEvents().after_frame_event.Register(
[this](Core::System&) { FlushFrameDump(); }, "FrameDumper");
}
FrameDumper::~FrameDumper()

View File

@ -84,8 +84,8 @@ bool FramebufferManager::Initialize()
return false;
}
m_end_of_frame_event =
AfterFrameEvent::Register([this](Core::System&) { EndOfFrame(); }, "FramebufferManager");
m_end_of_frame_event = GetVideoEvents().after_frame_event.Register(
[this](Core::System&) { EndOfFrame(); }, "FramebufferManager");
return true;
}

View File

@ -17,8 +17,8 @@ CustomShaderCache::CustomShaderCache()
m_async_uber_shader_compiler = g_gfx->CreateAsyncShaderCompiler();
m_async_uber_shader_compiler->StartWorkerThreads(1); // TODO
m_frame_end_handler = AfterFrameEvent::Register([this](Core::System&) { RetrieveAsyncShaders(); },
"RetrieveAsyncShaders");
m_frame_end_handler = GetVideoEvents().after_frame_event.Register(
[this](Core::System&) { RetrieveAsyncShaders(); }, "RetrieveAsyncShaders");
}
CustomShaderCache::~CustomShaderCache()

View File

@ -95,8 +95,8 @@ bool GraphicsModManager::Initialize()
g_ActiveConfig.graphics_mod_config->SetChangeCount(old_game_mod_changes);
g_graphics_mod_manager->Load(*g_ActiveConfig.graphics_mod_config);
m_end_of_frame_event =
AfterFrameEvent::Register([this](Core::System&) { EndOfFrame(); }, "ModManager");
m_end_of_frame_event = GetVideoEvents().after_frame_event.Register(
[this](Core::System&) { EndOfFrame(); }, "ModManager");
}
return true;

View File

@ -94,8 +94,8 @@ static void TryToSnapToXFBSize(int& width, int& height, int xfb_width, int xfb_h
Presenter::Presenter()
{
m_config_changed =
ConfigChangedEvent::Register([this](u32 bits) { ConfigChanged(bits); }, "Presenter");
m_config_changed = GetVideoEvents().config_changed_event.Register(
[this](u32 bits) { ConfigChanged(bits); }, "Presenter");
}
Presenter::~Presenter()
@ -195,14 +195,16 @@ void Presenter::ViSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height,
}
}
BeforePresentEvent::Trigger(present_info);
auto& video_events = GetVideoEvents();
video_events.before_present_event.Trigger(present_info);
if (!is_duplicate || !g_ActiveConfig.bSkipPresentingDuplicateXFBs)
{
Present(presentation_time);
ProcessFrameDumping(ticks);
AfterPresentEvent::Trigger(present_info);
video_events.after_present_event.Trigger(present_info);
}
}
@ -216,12 +218,14 @@ void Presenter::ImmediateSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_
present_info.reason = PresentInfo::PresentReason::Immediate;
present_info.present_count = m_present_count++;
BeforePresentEvent::Trigger(present_info);
auto& video_events = GetVideoEvents();
video_events.before_present_event.Trigger(present_info);
Present();
ProcessFrameDumping(ticks);
AfterPresentEvent::Trigger(present_info);
video_events.after_present_event.Trigger(present_info);
}
void Presenter::ProcessFrameDumping(u64 ticks) const
@ -925,7 +929,7 @@ void Presenter::DoState(PointerWrap& p)
if (p.IsReadMode() && m_last_xfb_stride != 0)
{
// This technically counts as the end of the frame
AfterFrameEvent::Trigger(Core::System::GetInstance());
GetVideoEvents().after_frame_event.Trigger(Core::System::GetInstance());
ImmediateSwap(m_last_xfb_addr, m_last_xfb_width, m_last_xfb_stride, m_last_xfb_height,
m_last_xfb_ticks);

View File

@ -46,8 +46,8 @@ bool ShaderCache::Initialize()
return false;
m_async_shader_compiler = g_gfx->CreateAsyncShaderCompiler();
m_frame_end_handler = AfterFrameEvent::Register([this](Core::System&) { RetrieveAsyncShaders(); },
"RetrieveAsyncShaders");
m_frame_end_handler = GetVideoEvents().after_frame_event.Register(
[this](Core::System&) { RetrieveAsyncShaders(); }, "RetrieveAsyncShaders");
return true;
}

View File

@ -19,10 +19,10 @@
Statistics g_stats;
static Common::EventHook s_before_frame_event =
BeforeFrameEvent::Register([] { g_stats.ResetFrame(); }, "Statistics::ResetFrame");
static Common::EventHook s_before_frame_event = GetVideoEvents().before_frame_event.Register(
[] { g_stats.ResetFrame(); }, "Statistics::ResetFrame");
static Common::EventHook s_after_frame_event = AfterFrameEvent::Register(
static Common::EventHook s_after_frame_event = GetVideoEvents().after_frame_event.Register(
[](const Core::System& system) {
DolphinAnalytics::Instance().ReportPerformanceInfo({
.speed_ratio = system.GetSystemTimers().GetEstimatedEmulationPerformance(),

View File

@ -463,8 +463,8 @@ private:
void OnFrameEnd();
Common::EventHook m_frame_event =
AfterFrameEvent::Register([this](Core::System&) { OnFrameEnd(); }, "TextureCache");
Common::EventHook m_frame_event = GetVideoEvents().after_frame_event.Register(
[this](Core::System&) { OnFrameEnd(); }, "TextureCache");
VideoCommon::TextureUtils::TextureDumper m_texture_dumper;
};

View File

@ -118,9 +118,11 @@ VertexManagerBase::~VertexManagerBase() = default;
bool VertexManagerBase::Initialize()
{
m_frame_end_event =
AfterFrameEvent::Register([this](Core::System&) { OnEndFrame(); }, "VertexManagerBase");
m_after_present_event = AfterPresentEvent::Register(
auto& video_events = GetVideoEvents();
m_frame_end_event = video_events.after_frame_event.Register(
[this](Core::System&) { OnEndFrame(); }, "VertexManagerBase");
m_after_present_event = video_events.after_present_event.Register(
[this](const PresentInfo& pi) { m_ticks_elapsed = pi.emulated_timestamp; },
"VertexManagerBase");
m_index_generator.Init();
@ -442,7 +444,7 @@ void VertexManagerBase::Flush()
if (m_draw_counter == 0)
{
// This is more or less the start of the Frame
BeforeFrameEvent::Trigger();
GetVideoEvents().before_frame_event.Trigger();
}
if (xfmem.numTexGen.numTexGens != bpmem.genMode.numtexgens ||

View File

@ -387,10 +387,10 @@ void CheckForConfigChanges()
}
// Notify all listeners
ConfigChangedEvent::Trigger(changed_bits);
GetVideoEvents().config_changed_event.Trigger(changed_bits);
// TODO: Move everything else to the ConfigChanged event
}
static Common::EventHook s_check_config_event = AfterFrameEvent::Register(
static Common::EventHook s_check_config_event = GetVideoEvents().after_frame_event.Register(
[](Core::System&) { CheckForConfigChanges(); }, "CheckForConfigChanges");

View File

@ -14,18 +14,6 @@ namespace Core
class System;
}
// Called when certain video config setting are changed
using ConfigChangedEvent = Common::HookableEvent<"ConfigChanged", u32>;
// An event called just before the first draw call of a frame
using BeforeFrameEvent = Common::HookableEvent<"BeforeFrame">;
// An event called after the frame XFB copy begins processing on the host GPU.
// Useful for "once per frame" usecases.
// Note: In a few rare cases, games do multiple XFB copies per frame and join them while presenting.
// If this matters to your usecase, you should use BeforePresent instead.
using AfterFrameEvent = Common::HookableEvent<"AfterFrame", Core::System&>;
struct PresentInfo
{
enum class PresentReason
@ -78,19 +66,37 @@ struct PresentInfo
std::vector<std::string_view> xfb_copy_hashes;
};
// An event called just as a frame is queued for presentation.
// The exact timing of this event depends on the "Immediately Present XFB" option.
//
// If enabled, this event will trigger immediately after AfterFrame
// If disabled, this event won't trigger until the emulated interface starts drawing out a new
// frame.
//
// frame_count: The number of frames
using BeforePresentEvent = Common::HookableEvent<"BeforePresent", PresentInfo&>;
struct VideoEvents
{
// Called when certain video config setting are changed
Common::HookableEvent<"ConfigChanged", u32> config_changed_event;
// An event that is triggered after a frame is presented.
// The exact timing of this event depends on backend/driver support.
using AfterPresentEvent = Common::HookableEvent<"AfterPresent", PresentInfo&>;
// An event called just before the first draw call of a frame
Common::HookableEvent<"BeforeFrame"> before_frame_event;
// An end of frame event that runs on the CPU thread
using VIEndFieldEvent = Common::HookableEvent<"VIEndField">;
// An event called after the frame XFB copy begins processing on the host GPU.
// Useful for "once per frame" usecases.
// Note: In a few rare cases, games do multiple XFB copies per frame and join them while
// presenting.
// If this matters to your usecase, you should use BeforePresent instead.
Common::HookableEvent<"AfterFrame", Core::System&> after_frame_event;
// An event called just as a frame is queued for presentation.
// The exact timing of this event depends on the "Immediately Present XFB" option.
//
// If enabled, this event will trigger immediately after AfterFrame
// If disabled, this event won't trigger until the emulated interface starts drawing out a new
// frame.
//
// frame_count: The number of frames
Common::HookableEvent<"BeforePresent", PresentInfo&> before_present_event;
// An event that is triggered after a frame is presented.
// The exact timing of this event depends on backend/driver support.
Common::HookableEvent<"AfterPresent", PresentInfo&> after_present_event;
// An end of frame event that runs on the CPU thread
Common::HookableEvent<"VIEndField"> vi_end_field_event;
};
VideoEvents& GetVideoEvents();

View File

@ -29,7 +29,10 @@ WidescreenManager::WidescreenManager()
"Invalid suggested aspect ratio mode: only Auto, 4:3 and 16:9 are supported");
}
m_config_changed = ConfigChangedEvent::Register(
auto& system = Core::System::GetInstance();
auto& video_events = system.GetVideoEvents();
m_config_changed = video_events.config_changed_event.Register(
[this](u32 bits) {
if (bits & (CONFIG_CHANGE_BIT_ASPECT_RATIO))
{
@ -45,10 +48,9 @@ WidescreenManager::WidescreenManager()
"Widescreen");
// VertexManager doesn't maintain statistics in Wii mode.
auto& system = Core::System::GetInstance();
if (!system.IsWii())
{
m_update_widescreen = AfterFrameEvent::Register(
m_update_widescreen = video_events.after_frame_event.Register(
[this](Core::System&) { UpdateWidescreenHeuristic(); }, "WideScreen Heuristic");
}
}