bleubleu wrote:
Wow, copying code in RAM and executing it? I've honestly never thought of that. Not sure that's a good fit for the design of my engine, but that's really original!

Not really
that original. It's common enough, so that cc65 has a built-in functionality for this. Also, that's the way the C64 uses its code since it cannot run it directly from the disc due to the slow speed.
I believe "The Legend of Zelda" does this as well because you see a whole bunch of weird values in the battery file when you simply start the game.
Also, how can this be a good or a bad fit for an engine? It's not like this will influence your general design, so I think it's always neutral. As long as you have a bunch of RAM left, it can always be used, no matter how the rest of your code is structured.
You simply dedicate a certain SEGMENT (doesn't even have to be a whole bank, just one SEGMENT in a MEMORY block) to your RAM:
Code:
MEMORY
{
WRAM: type = rw, start = $6000, size = $2000;
# Here we store the code that shall go into RAM.
# Of course, this bank can also still be used
# for other stuff if your RAM code is much less than 8 KB.
BANK_RAM: type = ro, start = $8000, size = $2000, file = %O, fill = yes;
}
SEGMENTS
{
# Variables that need to be saved between two game sessions, i.e. savestates.
BATTERY: load = WRAM, type = bss;
# Generic additional RAM variables if the regular console RAM isn't enough.
BSS_EX: load = WRAM, type = bss;
# Code executed in RAM.
# Note the difference between "load =" and "run =".
RAM_CODE_AND_RODATA: load = BANK_RAM, type = ro, run = WRAM, define = yes;
}
And then you do this in your initialization, for example before the second vblank:
Code:
LDA #BankRam
JSR SwitchBank
LDA #<__RAM_CODE_AND_RODATA_RUN__
STA Pointer1 + 0
LDA #>__RAM_CODE_AND_RODATA_RUN__
STA Pointer1 + 1
LDA #<__RAM_CODE_AND_RODATA_LOAD__
STA Pointer2 + 0
LDA #>__RAM_CODE_AND_RODATA_LOAD__
STA Pointer2 + 1
LDY #0
@copyCodeToRamLoop:
LDA (Pointer2), Y
STA (Pointer1), Y
INC Pointer1 + 0
BNE @pointer1IncrementEnd
INC Pointer1 + 1
@pointer1IncrementEnd:
INC Pointer2 + 0
BNE @pointer2IncrementEnd
INC Pointer2 + 1
@pointer2IncrementEnd:
LDA Pointer2 + 1
CMP #>(__RAM_CODE_AND_RODATA_LOAD__ + __RAM_CODE_AND_RODATA_SIZE__)
BNE @copyCodeToRamLoop
LDA Pointer2 + 0
CMP #<(__RAM_CODE_AND_RODATA_LOAD__ + __RAM_CODE_AND_RODATA_SIZE__)
BNE @copyCodeToRamLoop
Now you have up to 8 KB of additional always available space for anything. Just put the RAM_CODE_AND_RODATA segment declaration over anything that shall go into the RAM. It doesn't even matter which functions you use for this. You can change them on demand without altering anything else.
So, this is not even a concept that you need to plan in your engine design. It's a totally independent thing that always works as long as you have some WRAM left and as long as there's still room in any of your banks.
I'm doing this because in my opinion, it's the best if code is always available.
I would never do the approach where I split the code
and its data into small, logical units (like all music and the music engine go into one bank, sprite declarations and sprite handling go into another bank etc.).
Because if you have data that's larger than one bank (for example screen definitions in an action RPG), this means you have to mirror the code into several banks.
Instead, my goal is to put the whole code into the global bank and to
only use banks for data. (Maybe with the exception of completely separated code that will never ever interfer with anything else, like the title screen.)
That's why I welcome ways where you can maximize the always available code. So, I would never use 16 KB of switchable banks if I can cut it down to 8 KB while leaving the other 8 KB constant. In fact, if MMC3 allowed for 4 KB banks, I would leave three of them constant and only use one for switchable stuff.
In my opinion, having code always available is better than grouping code along with its corresponding data into banks. This way, it's less conscious planning.
If you find out that one of your functions needs stuff from two different banks where it only needed stuff from one bank before, you don't have to reorganize everything. The function can still switch the banks on demand, without the fear that the bank switch will pull the function itself away from the execution.