Skip to content

Add kmCondBranch and kmCondCall hooks#59

Open
RoadrunnerWMC wants to merge 1 commit intoTreeki:masterfrom
RoadrunnerWMC:cond_branch_call
Open

Add kmCondBranch and kmCondCall hooks#59
RoadrunnerWMC wants to merge 1 commit intoTreeki:masterfrom
RoadrunnerWMC:cond_branch_call

Conversation

@RoadrunnerWMC
Copy link
Collaborator

@RoadrunnerWMC RoadrunnerWMC commented Mar 16, 2026

Status: Fully tested, ready to merge.


This PR adds new hook types kmCondBranch and kmCondCall, which work as you'd expect:

kmCondBranch(addr, original, ptr);  // if *addr == original, put `b ptr` at addr
kmCondCall(addr, original, ptr);    // if *addr == original, put `bl ptr` at addr

The conditional value is a 32-bit integer, as in kmCondWrite32. This is expected to be an encoded PPC instruction.

A concrete use case for this is the revamped Super Mario Galaxy / Super Mario Galaxy 2 Kamek loader I'm currently working on. I'm sure it'll be useful more broadly, too.

Kamekfile format and loader changes

To support this new feature, the loader is updated to use the new Kamekfile v3 format, which adds new kCondBranch and kCondBranchLink command types. No changes to game harnesses are required.

It's worth pointing out that #54 also increments the Kamekfile version field, currently to 3. Whichever PR is merged second will have its version number changed to 4 instead.

"Def" versions

"Def" versions of these conditional calls can be easily added in a follow-up PR, if we want to.

Naming

This naming (kmCondBranch, kmCondCall) is consistent with other Kamek hooks. However, it's worth pointing out that "conditional branch" is already an established phrase with a different meaning (e.g. beq), which is kind of unfortunate.

Implementation notes

Rather than copypaste all the conditional-write code from the WriteCommand class to BranchCommand, which gets very hairy for certain output formats, I instead simplified the latter by making it delegate output generation to an equivalent WriteCommand when it can. This change fully passes my test suite (#52), so I believe it works correctly.

Discussion: Format of "original" value

This PR implements (as an example):

kmCondCall(0x800db084, 0x4815cc8d, mySinFIdx);

Rather than something like:

kmCondCall(0x800db084, { bl SinFIdx__Q24nw4r4mathFf }, mySinFIdx);

#54 adds the ability to write inline assembly injected into game-code addresses, and it might be tempting to adapt that technique to this case. However -- setting aside the fact that it'd be much more complicated to implement -- I don't think it's actually a good idea.

Potential benefits

Here are all of the benefits I can think of for being able to write inline assembly as in #54 (ignoring the "memory savings" benefit mentioned in that PR since it obviously doesn't apply here):

  • Readability: Assembly is more readable than hex values.
  • Editability: The instructions can be easily edited as source code, rather than having to re-assemble any changes manually.
  • Relocations for custom code: The instructions can link to other custom code, no matter where it gets loaded to.
  • Relocations for portability: Kamek can relocate the code properly, no matter where it's placed depending on the game version.

However, in this case, the "inline assembly" isn't actually custom code, but an instruction from the original game, which is a frozen, unchanging target. So here's how those arguments apply to this case:

  • Readability: Probably applies? I think it's debatable, though.
  • Editability: Doesn't apply, since the instruction will never need to be edited, except to change the target address.
  • Relocations for custom code: Doesn't apply, since original game code never links to custom code.
  • Relocations for portability: This one is tricky. In principle, this should be useful, as it would let you convert something like:
    kmCondCall(0x800db084, 0x4815cc8d, mySinFIdx);  // P (800db084)
    kmCondCall(0x800db084, 0x4815cc3d, mySinFIdx);  // E (800daf94)
    kmCondCall(0x800db084, 0x4815cacd, mySinFIdx);  // J (800daf14)
    // K and W don't have this call at all, hence why we're doing it conditionally

    To the example from earlier:

    kmCondCall(0x800db084, { bl SinFIdx__Q24nw4r4mathFf }, mySinFIdx);  // P, E, J
    // K and W don't have this call at all, hence why we're doing it conditionally

    In fact, the first version above wouldn't even work, as I don't believe Kamek currently lets you define multiple writes to the same address, even if they're conditional. (Maybe that should be changed, but obviously that's out of scope here.) So the second version would be both shorter and would actually work as intended.

    However, I believe the main use case for conditional writes is to make static patches that work with multiple game versions, such as Kamek loader harnesses. And static patches that work across game versions don't use address porting at all, so this benefit wouldn't apply. On the other hand, since it's not currently possible to mark writes in dynamic patches as only applying to specific game versions, conditional writes can still have uses in that mode.

    All in all, this seems like an argument in favor of the inline-assembly syntax, although it's hard to say how much the hooks would actually be used this way in practice.

Syntax issues

Assembly instructions can have commas (e.g. li r3, 0), which I imagine might confuse the preprocessor as it tries to figure out the number of arguments to the hypothetical inline-assembly versions of the macros:

kmCondCall(0x800db084, { li r3, 0 }, mySinFIdx);

->

  • 0x800db084
  • { li r3
  • 0 }
  • mySinFIdx

Though I haven't tested it to see if it actually works that way.

Difficulty of implementation

As previously mentioned, this would be much more complicated to implement, which I don't think should be overlooked. It's not just a matter of "would it be better", but also "would it be worth adding that much more complexity to the linker, which would have to be maintained and supported forever."

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.

1 participant