Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DolphinQt: Properly lock CPU before accessing emulated memory #11554

Merged
merged 2 commits into from Feb 13, 2023

Conversation

JosJuice
Copy link
Member

This fixes a problem I was having where using frame advance with the debugger open would frequently cause panic alerts about invalid addresses due to the CPU thread changing MSR.DR while the host thread was trying to access memory.

To aid in tracking down all the places where we weren't properly locking the CPU, I've created a new type (in Core.h) that you have to pass as a reference or pointer to functions that require running as the CPU thread.

@JosJuice JosJuice marked this pull request as draft February 12, 2023 10:07
@JosJuice
Copy link
Member Author

Work in progress. Dolphin seems to deadlock when exiting emulation.

This fixes a problem I was having where using frame advance with the
debugger open would frequently cause panic alerts about invalid addresses
due to the CPU thread changing MSR.DR while the host thread was trying
to access memory.

To aid in tracking down all the places where we weren't properly locking
the CPU, I've created a new type (in Core.h) that you have to pass as a
reference or pointer to functions that require running as the CPU thread.
This avoids a pseudo infinite loop where CodeWidget::UpdateCallstack
would lock the CPU in order to read the call stack, causing the CPU to
call Host_UpdateDisasmDialog because it's transitioning from running to
pausing, causing Host::UpdateDisasmDialog to be emitted, causing
CodeWidget::Update to be called, once again causing
CodeWidget::UpdateCallstack to be called, repeating the cycle.

Dolphin didn't go completely unresponsive during this, because
Host_UpdateDisasmDialog schedules the emitting of Host::UpdateDisasmDialog
to happen on another thread without blocking, but it was stopping certain
operations like exiting emulation from working.
@JosJuice JosJuice marked this pull request as ready for review February 12, 2023 11:50
@JosJuice
Copy link
Member Author

Okay, that problem should be fixed now.

@AdmiralCurtiss
Copy link
Contributor

AdmiralCurtiss commented Feb 12, 2023

A RAII class for PauseAndLock() definitely makes sense, and I understand why you'd want to signal to the user that 'hey, this function is only supposed to be called from the CPU thread, so prove you're that by giving me this object', but... it's kinda weird to have a parameter that does absolutely nothing in most functions it's passed to.

@JosJuice
Copy link
Member Author

Yeah, I suppose it is a little weird, but this is the best way I could think of to at compile time ensure that the caller has made an effort to lock the CPU. I couldn't have found all the sites that required locking to be added without it.

If having that parameter is too much of a bother, I could remove it now that I've found all the sites that require locking... but then the risk is high that we will introduce the problem again when adding some new debugger feature.

@AdmiralCurtiss
Copy link
Contributor

Yeah, it's probably fine to keep it, I don't really have a better idea either.

@phire
Copy link
Member

phire commented Feb 12, 2023

It feels a little weird that sometimes the guard is a pointer and sometimes it's a reference.

Potential idea: What if you created a second object. Call it something like DynamicThreadGuard that may or may not be paused. Make CpuThreadGuard implicitly or explictly convertible to a DynamicThreadGuard& so passing a CpuThreadGuard to those functions works.

@JosJuice
Copy link
Member Author

Something like this? Can squash if desired.

(The extra Core.h includes you see in the commit are needed for performing the implicit conversion.)

@AdmiralCurtiss
Copy link
Contributor

That seems worse to me, now we're actually back-and-forth converting a thing that's never used...

@phire
Copy link
Member

phire commented Feb 12, 2023

It uses inheritance and doesn't actually get converted. Zero runtime cost.

@JosJuice
Copy link
Member Author

There is a runtime cost when we call the virtual functions.

@JosJuice
Copy link
Member Author

I've removed the commit again.

What I would've wanted is really something like std::optional<const CPUThreadGuard&>, but C++ doesn't allow optional references. Pointers are in some sense the closest you can get.

@AdmiralCurtiss
Copy link
Contributor

Also here: I haven't reviewed this but I'd rather get this merged before anyone creates merge conflicts with this, since this looks pretty annoying to keep updated otherwise. Are you okay with the design as-is, @phire?

@phire
Copy link
Member

phire commented Feb 13, 2023

It's ok.

@phire phire merged commit a4729a0 into dolphin-emu:master Feb 13, 2023
14 checks passed
@phire
Copy link
Member

phire commented Feb 13, 2023

Commit 7cecb28 breaks gecko codes

@JosJuice JosJuice deleted the host-lock-cpu branch February 13, 2023 20:25
@@ -18,6 +18,7 @@
#include <fmt/printf.h>

#include "Common/Align.h"
#include "Common/Assert.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit doesn't seem to add any assertions to MemoryViewWidget.cpp. Was this include intended to do something? (I noticed this because it caused a merge conflict on #10957)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must be a leftover from an earlier version of this PR.

const std::optional<u32> addr =
const std::optional<u32> target_addr =
PowerPC::debug_interface.GetMemoryAddressFromInstruction(code_line);

if (addr)
QApplication::clipboard()->setText(QStringLiteral("%1").arg(*addr, 8, 16, QLatin1Char('0')));
{
QApplication::clipboard()->setText(
QStringLiteral("%1").arg(*target_addr, 8, 16, QLatin1Char('0')));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be using if (target_addr.has_value())? Now it's checking if GetContextAddress() != 0.

const std::optional<u32> addr =
const std::optional<u32> target_addr =
PowerPC::debug_interface.GetMemoryAddressFromInstruction(code_line);

if (addr)
emit ShowMemory(*addr);
emit ShowMemory(*target_addr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

double result;
{
Core::CPUThreadGuard guard;
m_expr->param.func.context = &guard;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only affects the top level of the expression tree (and only if it happens to be a function call), so this mostly isn't working as intended. The concept of a context in the expr library doesn't map nicely to what you want to do here. If this were something performance critical, I think the right fix would be to add a context parameter to expr_eval(). However, given that constructing guards is so cheap after the initial lock, rather than trying so hard to pass this one around it should be fine to just construct additional guards in the functions that need them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching that! I wasn't entirely sure if I was correctly understanding how the context worked.

Addressed in PR #11602.

JosJuice added a commit to JosJuice/dolphin that referenced this pull request Feb 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
5 participants