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

Fix on bus read issue (#3813) #3821

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

VelpaChallenger
Copy link

For BSNES core, signatures were updated and data value was passed to the read callback after reading from the bus. For genplus-gx, since all callbacks (read/write/exec) use the same signature from mem_cb, I had to pass the value to all three of them. And since this could potentially cause some unwanted behavior, I also updated the values passed to the write and exec callbacks inside the core (I divided each update in a separate commit so it's easier to follow and understand).

Check if completed:

- related to issue TASEmulators#3813
- update signatures, send returned value from bus read as data to read callback
- related to issue TASEmulators#3813
- update signatures, create new value variable in each of the memory read core functions, pass it to the callback and return it instead of the inline calculations. Also, pass val to write and exec callbacks in IDebuggable since they all use the same mem_cb signature and it would break otherwise. I want to update write and exec callbacks in next commit though to ensure nothing unexpected happens.
Copy link
Collaborator

@Morilli Morilli left a comment

Choose a reason for hiding this comment

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

Changes look good from what I can see without actually testing them.

@VelpaChallenger
Copy link
Author

Something important to keep in mind is that there's a trade-off to these changes, and that is that a memory.writebyte inside a memory read callback function will not write the value before the reading by the core, so if someone wanted to try to force a value to be read by the game (as a form of stronger freezing if you will), it won't be possible anymore, since now the callback is called after the bus is read (albeit still before the game reads it). I don't know how many people actually do this, but just as a heads-up.

@Morilli
Copy link
Collaborator

Morilli commented Nov 12, 2023

That's actually a valid point, and it reinforces the point that there IS a "before" and "after" state when it comes to reading values, and the decision on where the callback is called has an actual impact on how that callback can be used.

Quoting myself from earlier:

I'd definitely appreciate an update to the docs to clarify for both bizhawk devs and lua devs how exactly those functions should work.

@YoshiRulz I do believe the "before" part for the callback is correct and matches expectations, we just need to decide what to do with the val parameter and how it should get populated:

[LuaMethod("on_bus_read", "Fires immediately before the given address is read by the core. Your callback can have 3 parameters {{(addr, val, flags)}}. {{val}} is the value read. If no address is given, it will fire on every memory read.")]

@YoshiRulz
Copy link
Member

I'd definitely appreciate an update to the docs to clarify for both bizhawk devs and lua devs how exactly those functions should work.

[...] we just need to decide what to do with the val parameter and how it should get populated

I don't understand your question.

Fires immediately before the given address is read by the core. [...] val is the value read.

...seems pretty clear to me. Are you saying we should make that even clearer, maybe "val is the value to be read"? Or is it that existing implementations don't match the docs?


so if someone wanted to try to force a value to be read by the game (as a form of stronger freezing if you will), it won't be possible anymore

In our discussions on Discord re: possible implementations, this never came up. Freezing needs to remain functional—and I believe if you've broken it for Lua then it must also be broken for cheats.

@Morilli
Copy link
Collaborator

Morilli commented Nov 13, 2023

Are you saying we should make that even clearer, maybe "val is the value to be read"? Or is it that existing implementations don't match the docs?

Both, probably. First of all yes, val is the value to be read is the only way that makes sense to me.
Also, I assume no core will actually implement both conditions "correctly" at the moment, aka call the callback before the value is read (and allow for manipulation of the value to be read before it IS read), and also provide the value to be read in the callback.

Making it clearer in the docs what's important would help here.

@VelpaChallenger
Copy link
Author

In our discussions on Discord re: possible implementations, this never came up. Freezing needs to remain functional—and I believe if you've broken it for Lua then it must also be broken for cheats.

I think there's a misunderstanding: freezing still works as usual and hasn't been affected in any way. What I meant with the wording "stronger freezing" is that when you freeze a memory address using cheats (traditional way), you are not actually freezing the memory address in the strict sense of the word, or at least not the way I'd expect. Whenever a frame advances, if the game's code updates the memory address, it will be updated. Say for example that you freeze 0xC676 when it has stored a 0xC value, if there's a line like move.b D0, ($C676), and then there's another line move.b ($C676), ($C284), then 0xC284 is not going to be populated with the value 0xC, rather it's going to be populated with whatever value was in D0. That's what I mean when I say it's not a freeze in the strict sense of the word. It's only going to get updated to 0xC at the end of the frame, but during it, freezing has no effect. However, when using memory callbacks, you can do something like memory.writebyte(0xC676, 0xC) inside your callback and then when move.b ($C676), ($C284) is executed, the value 0xC is going to be passed to 0xC284 because that is the value that is going to be read at 0xC676.

Also, the way I see it, there are 3 key moments to have in mind:

  1. The moment the bus is read
  2. The moment the game reads the value
  3. The moment the callback is run

The moment the bus is read is for BSNES when this line auto data = bus.read(address, r.mdr); is executed. The moment the game reads the value is after the function used for accessing memory values returns (for BSNES, that would be when CPU::read returns). And the moment the callback is run is, well, when it's called by the core.

My point being that as I mentioned in the issue comments, while I agree with Morilli that the decision on where the callback is called has an actual impact on how that callback can be used, I still don't think there is really a "before" and an "after" state. The value still hasn't been sent when the callback is run, in fact, one possible way to go around this is to call bus.read again after calling the callback and then send that as the return value of CPU::read (for BSNES). That would be possible because the game's code still didn't receive the value at that point in time, so it's still before.

I do believe before the given address is read by the core should be changed to before the given address is read by the game's code, though. val is the value to be read sounds more accurate, but it's still not fully accurate given what we are discussing here and how memory callbacks cannot be used for the stronger freezing I was talking about.

Also, speaking of which. This detail slipped my mind earlier, but for the existing implementations, like here:

if (MemoryCallbacks.HasReads)
{
uint flags = (uint)(MemoryCallbackFlags.CPUZero | MemoryCallbackFlags.AccessRead);
MemoryCallbacks.CallMemoryCallbacks(addr, ret, flags, "System Bus");
}
DB = ret;
return ret;
memory callbacks already cannot be used for the stronger freezing. So in that sense, nothing to really worry about.
(As a positive of this implementation, you can know how many bytes are actually being read by the game (so val will be 1 byte if just 1 byte is being read, 2 bytes if 2 bytes are being read, etc.). This has been very useful to me for reverse engineering.)

@YoshiRulz
Copy link
Member

This discussion is continuing on Discord, but I want to apologise for my earlier assertion that you'd broken freezing. Freezing is, of course, already "broken" in that sense, and has been forever since it's implemented as pokes.

case WatchSize.Byte:
_watch.Poke(((ByteWatch)_watch).FormatValue((byte)_val));
break;
case WatchSize.Word:
_watch.Poke(((WordWatch)_watch).FormatValue((ushort)_val));
break;
case WatchSize.DWord:
_watch.Poke(((DWordWatch)_watch).FormatValue((uint)_val));
break;

(This method is called twice in StepRunLoop_Core, both before and after FrameAdvance.)

@Morilli Morilli self-requested a review March 19, 2024 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants