N64 On The Pandora


Exophase said:
I hope we can turn every thread about Mupen64plus into a coding discussion ;D
LOL... people keep asking why certain games don't work, but they don't like the answers :)

Exophase said:
- Okay, I assume the emulated page table starts after #offset, but how important are the things in that region, such that you'd have fp point there instead of + offset forward? If you generally access things via immediate offsets from that point then it'd just be with negative offsets, but I think it's unlikely that you're doing anything that hits more frequently than memory code. But, this could be a compatibility issue against your normal mode of operation. If the offset isn't required you can load from (fp + ((address >> 12) << 2)), not that I need to tell you that.
fp contains a pointer to some local variables at different offsets, and there would be enough space to put the mapping table at the end. This saves an instruction compared to loading the address with movw/movt.
There are various ways to do it, eg
Code:
   add r2, fp, #offset
   lsr r3, r1, #12
   ldr r2, [r2, r3 lsl #2]
but either way you end up with stalls due to shifts and loads.

Exophase said:
So if possible it'd be good to schedule the address generation in advance where you can. ... you probably already knew this too, but just in case.
It can be difficult to generate the address in advance, often reads/writes occur one after another, such as
Code:
 LW r3, 4(r2)
 LW r4, 8(r2)
 LW r5, 128(r6)
Even if you have enough registers to do it, you also need to make sure there are no intervening branch targets. About the only case where a significant improvement can be made is the following:
Code:
 LUI r1, 89ab
 LW  r2, 1234(r1)
In that case you can precalculate the address of the page table entry and avoid the shift. Stores also check for self-modifying code, so it may be possible to schedule instructions around that.

Exophase said:
- There's an alternative for MMU or complex memory space emulation that hlide suggested to me once, and I use in the MIPS version of gpSP.. I'm not sure yet if it would work out better for you or not, but the idea is to check if the entry is the same page as it was last time, and if so, use a direct routine. I think this would probably work better on x86 or a platform with larger immediates than on ARM, because you need to do a pretty large compare and immediate add. For ARM I think it'd be something like this:
Code:
 ubfx r2, r1, #12, #20
 movt r0, #offset_low
 movt r3, #last_memory_low
 movw r0, #offset_high
 movw r3, #last_memory_high
 cmp r2, r3
 blne memory_miss
 ldreq r0, [r0, r1]
That may work for GBA emulation without a MMU, but with 4K pages the miss rate is going to be way too high. (Consider a loop incrementing a pointer that crosses a page boundary.)

Exophase said:
Of course, this only works well if the memory accesses hit the same pages usually. Otherwise it incurs the costs of self-modifying code to update the offsets, which probably wouldn't be quite as bad if it didn't require going through the kernel. This uses more code space and worse, more registers, but I think it'll usually take less cycles since it pairs much better/has fewer stalls. This is assuming that the ubfx takes one cycle, since the TRM doesn't mention it. It probably has the usual shift interlock.
ubfx r2, r1, #12, #20 is equivalent to lsr r2, r1, #12... I doubt the ubfx encoding is any faster.
 
Last edited by a moderator:
ashdjones said:
While we're all here asking, is the Fzero-X glitch serious? Its my favourite racing game ever!
What glitch?

kin said:
what about waverace64?
Looks fine to me.

HackModford said:
Hey Ari64! Do you think the enhanced texture packs will ever be possible?
Ask Adventus.
 
Last edited by a moderator:
Ari64 said:
Exophase said:
So if possible it'd be good to schedule the address generation in advance where you can. ... you probably already knew this too, but just in case.
It can be difficult to generate the address in advance, often reads/writes occur one after another, such as
Code:
 LW r3, 4(r2)
 LW r4, 8(r2)
 LW r5, 128(r6)
Even if you have enough registers to do it, you also need to make sure there are no intervening branch targets.
Do you mean that your code generator will always use strict basic blocks? Couldn't you accept that a sequence of instructions can be translated multiple times depending from where you enter it or would that make other things such as SMC more difficult?
 
Last edited by a moderator:
Laurent said:
Do you mean that your code generator will always use strict basic blocks? Couldn't you accept that a sequence of instructions can be translated multiple times depending from where you enter it or would that make other things such as SMC more difficult?
It will in some cases translate things differently depending on where you enter. I try to keep this to a minimum because it makes very inefficient use of the L1 cache to have this stuff translated multiple times.
 
Last edited by a moderator:
So when you translate a block you check whether each PC in that block has already been translated? If it's what you're doing I wonder how better/worse it is compared to having different translations. The typical trade-off that can't be decided unless one spends a good amount of effort benchmarking different programs.

I'd need to spend some time looking at your code... Is there some easy way to check mupen output without having to run a real ROM (which I don't have since I don't own any N64 game)?
 
Laurent said:
So when you translate a block you check whether each PC in that block has already been translated? If it's what you're doing I wonder how better/worse it is compared to having different translations. The typical trade-off that can't be decided unless one spends a good amount of effort benchmarking different programs.
Not every instruction, just in certain cases such as after an unconditional jump instruction.

It's in new_dynarec.c around line 6375:
Code:
      // Don't recompile stuff that's already compiled
      if(check_addr((u_int)addr+i*4+4)) done=1;
The usual case of this is a call/return. If a call (JAL instruction) is encountered, then it will continue translating at the next instruction. That way when the subroutine returns to the caller, execution continues at a line which is already in the cache. If for some reason it needs to retranslate the part of the block containing the call, then it will not continue past the call since there is no reason why you would want two copies of the return point.

Laurent said:
I'd need to spend some time looking at your code... Is there some easy way to check mupen output without having to run a real ROM (which I don't have since I don't own any N64 game)?
Try dexanoid or the roms from pdroms.de. Not all of them work though. Define assem_debug if you want to see the output.
 
Last edited by a moderator:
Ari64 said:
fp contains a pointer to some local variables at different offsets, and there would be enough space to put the mapping table at the end. This saves an instruction compared to loading the address with movw/movt.

The point I was trying to make was to have fp point to the mapping table and use negative indexing to reach the local variables. This is assuming that you have < 4KB of them.

Ari64 said:
but either way you end up with stalls due to shifts and loads.

lsl #2 in memory instructions is the same cost as no shift, it's a special case. The register offset is a separate dependency on the AGI, but with you modifying the base so soon before it really doesn't make a difference.

Ari64 said:
That may work for GBA emulation without a MMU, but with 4K pages the miss rate is going to be way too high. (Consider a loop incrementing a pointer that crosses a page boundary.)

Yes, if you're emulating the TLB behavior precisely. However, if the game has a fixed address mapping (as I referred to) and is just using the TLB to cache a page table then you can effectively ignore TLB evictions and keep mapping up the same table. Having a single large virtual address space is certainly the model I would expect games to follow. At any rate, you can still determine if the address space is modified, and if so flush the translation - and if it happens frequently, choose a different mapping strategy. So long as games don't rely on protecting and catching accesses to areas that were previously allowed you won't have a problem, and I don't suspect this is normal behavior for N64 games.

The only problem is if games have a process switching OS, but I can't really imagine why they'd want to incur this sort of needless expense.

Ari64 said:
ubfx r2, r1, #12, #20 is equivalent to lsr r2, r1, #12... I doubt the ubfx encoding is any faster.

That wasn't really the point, I used that in case you wanted to ignore any upper bits, which makes decent sense for the common MIPS address space. I just left the actual number open. Of course ubfx isn't faster than shift.
 
Last edited by a moderator:
I have always wondered if making your own n64 rom that runs specific code would help with optimising / figuring out problems with the emulator that normal debugging wouldn't help with. Instead of trying to understand what the n64 game is trying to do when it crashes/glitches etc? :)
 
MDave said:
I have always wondered if making your own n64 rom that runs specific code would help with optimising / figuring out problems with the emulator that normal debugging wouldn't help with. Instead of trying to understand what the n64 game is trying to do when it crashes/glitches etc? :)

Making test ROMs helps a lot if you're trying to determine particular aspects of the hardware. Of course, that usually means running it on the original hardware for comparison, but when just starting to implement basic functions of the machine it can be enough to test things based on how you expect them to work, or even compare it with other emulators.

I think that what you're describing is more like taking an N64 ROM and paring out all of the parts that aren't relevant to a bug until only the bug is left, so you can know specifically what's causing the problem. This is actually usually much harder than working with the ROM unmodified since it takes a lot of reverse engineering before you know what to change, and making those changes is involved work (and it's hard to know that you're not upsetting things in some subtle way).

You can't really make a ROM from scratch to fix a glitch in an N64 game because the entire problem is you don't know what the glitch is. If/once you understand that then the game is usually a perfectly suitable test case for a new implementation, and even if you could do a simpler synthetic test (although it'd be a lot of work just writing it) you wouldn't know if it really captures everything exactly like the real game does. And getting the real game working is much more important.
 
Last edited by a moderator:
Exophase said:
You can't really make a ROM from scratch to fix a glitch in an N64 game because the entire problem is you don't know what the glitch is. If/once you understand that then the game is usually a perfectly suitable test case for a new implementation, and even if you could do a simpler synthetic test (although it'd be a lot of work just writing it) you wouldn't know if it really captures everything exactly like the real game does. And getting the real game working is much more important.

But wouldn't it help in benchmarking different solutions when trying to figure out the most optimized way of doing things, to make the emulator run faster? Unless it takes longer to do this then using existing ROMs.
 
Last edited by a moderator:
Awakening said:
But wouldn't it help in benchmarking different solutions when trying to figure out the most optimized way of doing things, to make the emulator run faster? Unless it takes longer to do this then using existing ROMs.

Not really. The performance of real games is what you care about, and benchmarks don't take that long.
 
Last edited by a moderator:
Yeah, I didn't explain it too well what I wanted to say :p but yeah you answered my question. I'm not savvy with emulation development.

I understand that you can't compare benchmarks to real games, because they could have a lot more going on under the hood like model conversion and skinning, sound decompressing and what not. But .. isolated tests of specific parts of the hardware might be useful? Like say, rendering 100,000 or so polygons onscreen with different texture blend modes that the n64 is capable of? I know that's more of the graphics plugin side of things, but still part of emulation :p
 
Ari64 said:
HackModford said:
Hey Ari64! Do you think the enhanced texture packs will ever be possible?
Ask Adventus.

Okay!
Hey Adventus! Do you think the enhanced texture packs will ever be possible?
 
Last edited by a moderator:
Exophase said:
The point I was trying to make was to have fp point to the mapping table and use negative indexing to reach the local variables. This is assuming that you have < 4KB of them.
There are separate mapping tables for reads and writes (pages can be write protected) so these need separate offsets. Maybe some games will be okay with a single table, I don't know.

Otherwise this would work, the local variables are about 1KB.

Exophase said:
Yes, if you're emulating the TLB behavior precisely. However, if the game has a fixed address mapping (as I referred to) and is just using the TLB to cache a page table then you can effectively ignore TLB evictions and keep mapping up the same table. Having a single large virtual address space is certainly the model I would expect games to follow. At any rate, you can still determine if the address space is modified, and if so flush the translation - and if it happens frequently, choose a different mapping strategy. So long as games don't rely on protecting and catching accesses to areas that were previously allowed you won't have a problem, and I don't suspect this is normal behavior for N64 games.
I am sure this optimization will work for some games. I doubt it will work in all cases though. I believe there are games which do demand paging, loading pages into RAM when a read from an unmapped page occurs.

Exophase said:
The only problem is if games have a process switching OS, but I can't really imagine why they'd want to incur this sort of needless expense.
I don't know of any, but I haven't tested every N64 game.
 
Last edited by a moderator:
Ari64 said:
There are separate mapping tables for reads and writes (pages can be write protected) so these need separate offsets. Maybe some games will be okay with a single table, I don't know.

Otherwise this would work, the local variables are about 1KB.

My current approach to unify two tables is this: the most significant bit set determines if something is writable or not, and all zeros determines that it can't be read from. When the remap entry is loaded it's shifted to the left by 1 (or added to itself). The carry flag is checked for stores, and the zero flag is checked for loads.

The one drawback to this approach is that it only allows read-write, read-only, or invalid - it doesn't allow write-only. But this is only an issue if the MMU in question supports write-only. If it does, you access a second table to catch false-positives on stores.

Ari64 said:
I am sure this optimization will work for some games. I doubt it will work in all cases though. I believe there are games which do demand paging, loading pages into RAM when a read from an unmapped page occurs.

You can still support this in the following fashion: by default, load and store code is emitted such that they check the page table entry, and if it's valid the routine is patched to the one I originally gave. This is only a problem if the start routine requires more bytes than the "usual" one I gave; chances are that instead it'll take less and will have to be padded out with nops. It does mean every load/store would have to be modified at least, but it was going to be this way anyway (unless you could reliably predict which page an access will use). This shouldn't be a big deal unless you end up cycling the code a lot, in which case you probably have bigger problems.

It's if they go back to being unmapped that you have a bigger problem, as you'll have to flush the translation cache. That is, unless you keep a list of all memory references keyed by which page they're assigned to.

Ari64 said:
I don't know of any, but I haven't tested every N64 game.

Should be OK if some games break it, it won't be too bad to detect and deal with in suitable alternative ways.
 
Last edited by a moderator:
Ari64 said:
ashdjones said:
While we're all here asking, is the Fzero-X glitch serious? Its my favourite racing game ever!
What glitch?

In Craig's video he says it crashes when you enter the game (he shows a clip of it running in demo mode).
 
Last edited by a moderator:
Exophase said:
Ari64 said:
There are separate mapping tables for reads and writes (pages can be write protected) so these need separate offsets. Maybe some games will be okay with a single table, I don't know.

Otherwise this would work, the local variables are about 1KB.

My current approach to unify two tables is this: the most significant bit set determines if something is writable or not, and all zeros determines that it can't be read from. When the remap entry is loaded it's shifted to the left by 1 (or added to itself). The carry flag is checked for stores, and the zero flag is checked for loads.

The one drawback to this approach is that it only allows read-write, read-only, or invalid - it doesn't allow write-only. But this is only an issue if the MMU in question supports write-only. If it does, you access a second table to catch false-positives on stores.
Well, the other drawback is that the shift/add will take an additional cpu cycle. I don't know what's worse, an additional cycle for every memory access or cache and register pressure from having two tables.

Exophase said:
Ari64 said:
I am sure this optimization will work for some games. I doubt it will work in all cases though. I believe there are games which do demand paging, loading pages into RAM when a read from an unmapped page occurs.

You can still support this in the following fashion: by default, load and store code is emitted such that they check the page table entry, and if it's valid the routine is patched to the one I originally gave. This is only a problem if the start routine requires more bytes than the "usual" one I gave; chances are that instead it'll take less and will have to be padded out with nops. It does mean every load/store would have to be modified at least, but it was going to be this way anyway (unless you could reliably predict which page an access will use). This shouldn't be a big deal unless you end up cycling the code a lot, in which case you probably have bigger problems.

It's if they go back to being unmapped that you have a bigger problem, as you'll have to flush the translation cache. That is, unless you keep a list of all memory references keyed by which page they're assigned to.
That's how I do branches, keep a list and then patch them. There are going to be a lot more loads/stores than branches, so it might not be worth it.
 
Last edited by a moderator:
Ari64 said:
Well, the other drawback is that the shift/add will take an additional cpu cycle. I don't know what's worse, an additional cycle for every memory access or cache and register pressure from having two tables.

You have to check the value after you load it no matter what you do. Right now you're doing it using a test. An add wouldn't take more cycles. You could even use a cmn if you don't have a register to write to.

Ari64 said:
That's how I do branches, keep a list and then patch them. There are going to be a lot more loads/stores than branches, so it might not be worth
it.

Can you quantify that statement with counts of branches vs memory access? I'm sure the latter will be more, but I'm interested in how much. Personally, I don't think it's worth keeping lists of all branches either, which I assume is for self modifying code patching. But this is all quite secondary since the only function of this is to speed up games that heavily change their page tables, when you could just as well fall back on a different recompilation strategy.
 
Last edited by a moderator:
Back
Top