don't click here

Sonic & Knuckles Collection C port

Discussion in 'Engineering & Reverse Engineering' started by BenoitRen, Jan 11, 2024.

  1. MainMemory

    MainMemory

    Kate the Wolf Tech Member
    4,742
    338
    63
    SonLVL
    Are you familiar with S3K's non-player DPLC format? It's described here on the wiki.
    As for the first word being stored as big endian, I think a possible reason is that the DPLCs were written into the game code as macros, and for whatever reason the macro writes the count as a set of two bytes instead of a word (I don't think 257 DMA transfers at once is even possible on MD), and the porting team, rather than adjusting the macro, simply chose to rotate the word to the correct order in the function. This rotation does not exist in the original MD version, and ideally you could find a way to remove it from your version as well, by correcting the source data instead.
     
    • Like Like x 1
    • Informative Informative x 1
    • List
  2. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,249
    1,420
    93
    your mom
    "pData->unknown" points to the actual graphics data, while "pData->tbl" points to the "DPLC" data that dictates which tiles from the graphics should be loaded for a sprite frame. And yeah, like MainMemory said, the weird rotation is most definitely some sort of endian conversion that doesn't exist in the original function (68000 is big endian, x86 is little endian).

    For each entry in a set of DPLC data for a sprite frame, d3 contains 2 values: The first 12 bits indicate the tile offset within the graphics data to start pulling from, which gets masked out and converted into a raw byte offset (with it already being positioned 4 bits left of the LSB, it just the bit mask and then 1 more bit shift). The last 4 bits indicate the number of tiles to load from that offset, minus 1 (0 means load 1 tile, 1 means load 2 tiles, etc.). That then gets converted into the number of 16-bit words to copy. That is then also used to advance the graphics data offset by adding it twice (again, length in 16-bit words).

    In the original game, "Copy_Data_To_VRAM" is a DMA queueing function. On the Genesis, you can set up the VDP to perform a DMA transfer from 68000 memory to VDP memory. In the case of "Copy_Data_To_VRAM", the queue is for loading graphics into VRAM. Register d1 is used to hold the source graphics data to copy, register d2 contains the VRAM address to copy into, and register d3 contains the number of 16-bit words to copy. The copy from "vram_adr" to "d2" is basically just a leftover with how the original game handled setting up parameters for "Copy_Data_To_VRAM".

    Code (ASM):
    1. Perform_DPLC:
    2.         moveq   #0,d0                   ; Get the sprite frame number
    3.         move.b  mapping_frame(a0),d0
    4.      
    5.         cmp.b   $3A(a0),d0              ; If it remains the same as before, don't do anything
    6.         beq.s   .end
    7.         move.b  d0,$3A(a0)
    8.      
    9.         movea.l (a2)+,a3                ; Get graphics data address
    10.      
    11.         move.w  art_tile(a0),d4         ; Get object's sprite tile ID in VRAM
    12.         andi.w  #$7FF,d4
    13.         lsl.w   #5,d4                   ; Convert to VRAM address
    14.      
    15.         movea.l (a2)+,a2                ; Get sprite frame's DPLC script
    16.         add.w   d0,d0
    17.         adda.w  (a2,d0.w),a2
    18.      
    19.         move.w  (a2)+,d5                ; Get number of DMA transactions
    20.         moveq   #0,d3                   ; d3 is used for longword operations, so make sure it's cleared
    21.  
    22. .loop:
    23.         move.w  (a2)+,d3                ; Get offset in graphics data address to copy from
    24.         move.l  d3,d1
    25.         andi.w  #$FFF0,d1               ; Mask out and convert to byte offset
    26.         add.w   d1,d1                   ; Value is already 4-bits past LSB, just do 1 more shift
    27.         add.l   a3,d1                   ; Add base graphics data address
    28.      
    29.         move.w  d4,d2                   ; Get the destination VRAM address (for Add_To_DMA_Queue)
    30.      
    31.         andi.w  #$F,d3                  ; Get the number of tiles to load
    32.         addq.w  #1,d3
    33.         lsl.w   #4,d3                   ; Convert to number of 16-bit words to transfer
    34.  
    35.         add.w   d3,d4                   ; Advance VRAM address for next DMA queue
    36.         add.w   d3,d4
    37.    
    38.         jsr     Add_To_DMA_Queue        ; Add to DMA queue (uses d1, d2, and d3 as parameters)
    39.  
    40.         dbf     d5,.loop                ; Loop until all DPLC entries are processed
    41.  
    42. .end:
    43.         rts

    Here's an example of how "pData" is set up for the Rhinobot in Angel Island:
    Code (ASM):
    1.         lea     DPLCPtr_AIZRhinobot(pc),a2
    2.         jsr     Perform_DPLC(pc)
    Code (ASM):
    1. DPLCPtr_AIZRhinobot:
    2.         dc.l    ArtUnc_AIZRhinobot
    3.         dc.l    DPLC_Rhinobot
     
    Last edited: Mar 17, 2024
    • Like Like x 1
    • Informative Informative x 1
    • List
  3. BenoitRen

    BenoitRen

    Tech Member
    410
    183
    43
    Thanks, both of you, that was very insightful!

    Do we have any idea of what DPLC was called in the original source code? I mean, that's a term the community came up with, right?
     
  4. Kilo

    Kilo

    That inbetween sprite from S&K's title screen Member
    315
    326
    63
    Canada
    wrtpat. Short for write pattern.
     
  5. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,249
    1,420
    93
    your mom
    Just to confirm that, in the Sonic 2 Nick Arcade symbol data, Sonic's DPLC data is called "playwrtpat", with each entry being labeled as "plwNNN" (NNN = frame number). Each DPLC entry is labeled as "foxwNNN" for Tails (the main label isn't found, but it can be assumed it's "foxwrtpat" or something like that).
     
  6. Brainulator

    Brainulator

    Regular garden-variety member Member
    Tee-hee.
    upload_2024-3-17_14-20-52.png
    Oh, and apparently, plwpat also exists and is another label for playwrtpat. There's also a label for playwrt down below, which is what Sonic uses to do the DPLCs, as we call them.
     
  7. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,249
    1,420
    93
    your mom
    Ah, it seems that the TCRF page is missing data, because I couldn't find it there. Though, saying that playwrt "does" the DPLCs sounds a bit weird. I'd just describe it as the function that takes playwrtpat/plwpat and loads the appropriate tiles for the player's current sprite.
     
    Last edited: Mar 17, 2024
  8. BenoitRen

    BenoitRen

    Tech Member
    410
    183
    43
    Ahh, much better!
    Code (Text):
    1. void set_emy_wchg(sprite_status* pActwk, wrt_data* pData) {
    2.   if (pActwk->patno == SPRITE_STATUS_UCHAR(pActwk, 58)) return;
    3.   SPRITE_STATUS_UCHAR(pActwk, 58) = pActwk->patno;
    4.   unsigned short vram_adr = (pActwk->sproffset & 0x7FF) << 5;
    5.   unsigned short* wrtpat = pData->wrtpat[pActwk->patno];
    6.   short cnt = *wrtpat++; // this conversion from unsigned to signed should be safe
    7.  
    8.   do {
    9.     unsigned short instruction = *wrtpat++;
    10.     unsigned char* source = pData->pat + ((instruction & 0xFFF0) << 1);
    11.     unsigned short words = ((instruction & 0xF) + 1) * 16;
    12.     unsigned short destination = vram_adr;
    13.     vram_adr += words * 2;
    14.     Copy_Data_To_VRAM(source, destination, words);
    15.     --cnt;
    16.   } while (cnt != -1);
    17. }
     
  9. MainMemory

    MainMemory

    Kate the Wolf Tech Member
    4,742
    338
    63
    SonLVL
    I will say, coding in raw offsets like 58 is a very bad idea. If you can't make a proper struct (understandable considering you'd need like 500 variations), you could at least make an enum or set of defines so it's easy to move things later if necessary.
     
  10. BenoitRen

    BenoitRen

    Tech Member
    410
    183
    43
    I agree. I intend to revisit the way I use "scratch RAM", soon.
     
  11. Billy

    Billy

    RIP Oderus Urungus Member
    2,119
    179
    43
    Colorado, USA
    Indie games
    Not really related to the code directly, but you should have this as a comment in the code, if you don't already.
     
  12. BenoitRen

    BenoitRen

    Tech Member
    410
    183
    43
    What is "this" referring to? I don't think you mean that I should put as a comment that my understanding of DPLC was lacking. :)
     
  13. Brainulator

    Brainulator

    Regular garden-variety member Member
    There's actually quite a few symbols missing there, probably because they were copied-and-pasted from this post. I can provide my own list of labels I've found.
     
  14. Billy

    Billy

    RIP Oderus Urungus Member
    2,119
    179
    43
    Colorado, USA
    Indie games
    I was referring to the entirety of what I quoted. Best practice for comments is to say why, not what. You mentioned the reason for the inconsistent variable names in the function, that's the 'why' I noticed in this case. Though more important is possibly the note about what the function is called in the disassembly, in case you (or someone else, I forget if you said you're intending to open source this) needs it.

    EDIT: I hadn't noticed the variable names had been improved above. I don't know a ton about 68k assembly and even less about Sonic code, so I mentally skipped most of that, that's on me. :)
     
    Last edited: Mar 17, 2024
  15. BenoitRen

    BenoitRen

    Tech Member
    410
    183
    43
  16. BenoitRen

    BenoitRen

    Tech Member
    410
    183
    43
    With gost08 ported, I figured I could map out how it uses its "scratch RAM" (actfree):
    wiki: "Used as a timer/counter, upon completion the routine will jump to a pointer in $34."

    Here, it's used to decide when to attack.
    Okay, that's pretty clear.
    I guess this is set for other code to use once 0x2E reaches zero?
    No idea what this is for. It's also not for a ghost, but the ghost capsule.
    puka_puka uses a bit for book-keeping the floating logic.
    Counter for when a ghost appears?
    More values for puka_puka.
    These are the most cryptic to me, because I have no idea what DAT_08fff7c3 and DAT_08fffaad are. DAT_08fff7c3 is exclusive to gost08, save for one spot much further in the code, and DAT_08fffaad is a shared value.
     
    • Informative Informative x 1
    • List
  17. BenoitRen

    BenoitRen

    Tech Member
    410
    183
    43
    I don't know why, but in Ghidra there's only one external reference to DAT_08fff7c3. When I look at the Mega Drive game disassembly, I find more references. From looking at those, it seems to be related to the palette, and from there I make the link with the fact that the ghosts only start attacking once it's dark enough.

    But that's not why I'm posting today.

    Those who have read Sonic 3 Unlocked: The global animation system, part 2 will have an idea of how the animation system works. What you need to remember is that negative values in the animation sequence are control codes.

    The implementation of patchg when it comes to those control codes made me raise my raise my eyebrows:
    Code (Text):
    1. void patchg(sprite_status* p_actwk, signed char** p_chg_data) {
    2.   if (p_actwk->mstno != p_actwk->restart) {
    3.     p_actwk->restart = p_actwk->mstno;
    4.     p_actwk->patcnt = 0;
    5.     p_actwk->pattim = 0;
    6.   }
    7.   --p_actwk->pattim;
    8.   if (p_actwk->pattim > 0) return;
    9.   signed char* p_sp_data = p_chg_data[p_actwk->mstno];
    10.   p_actwk->pattim = p_sp_data[0];
    11.   signed char sp = p_sp_data[p_actwk->patcnt + 1];
    12.   if (sp >= 0) {
    13. LAB_0041eca2:
    14.     p_actwk->patno = sp;
    15.     CLEAR_BIT_0(p_actwk->actflg);
    16.     CLEAR_BIT_1(p_actwk->actflg);
    17.     p_actwk->actflg |= p_actwk->cddat1 & 3;
    18.     ++p_actwk->patcnt;
    19.     return;
    20.   }
    21.   ++sp;
    22.   if (sp == 0) {
    23.     p_actwk->patcnt = 0;
    24.     sp = p_sp_data[1];
    25.     goto LAB_0041eca2;
    26.   }
    27.   ++sp;
    28.   if (sp == 0) {
    29.     sp = p_sp_data[p_actwk->patcnt + 2];
    30.     p_actwk->patcnt -= sp;
    31.     sp = p_sp_data[p_actwk->patcnt - sp + 1];
    32.     goto LAB_0041eca2;
    33.   }
    34.   ++sp;
    35.   if (sp == 0) {
    36.     p_actwk->mstno = p_sp_data[p_actwk->patcnt + 2];
    37.     return;
    38.   }
    39.   ++sp;
    40.   if (sp == 0) {
    41.     p_actwk->r_no += 2;
    42.     p_actwk->pattim = 0;
    43.     ++p_actwk->patcnt;
    44.     return;
    45.   }
    46.   ++sp;
    47.   if (sp == 0) {
    48.     p_actwk->xposi = 32512 + LONG_LOW(p_actwk->xposi);
    49.   }
    50. }
    How does it check which control code it has after seeing it's a negative value? By incrementing it by 1 and checking if the result is zero. Over, and over again.

    There's a similar animation system that also has negative values as control codes, and that one works with a jump table. Which means I can turn it into a switch statement:
    Code (Text):
    1. static int chk_tpg_next(sprite_status* p_actwk, signed char pg, signed char* p_pg_data, unsigned char patcnt) {
    2.   int ret = 0;
    3.   pg = -pg;
    4.  
    5.   switch (pg) {
    6.     case PG_CONTROL_NULL:
    7.       break;
    8.     case PG_CONTROL_NEXT:
    9.       pg = p_pg_data[patcnt + 1];
    10.       p_pg_data = &p_pg_data[pg];
    11.       SPRITE_STATUS_PSCHAR(p_actwk, 48) = p_pg_data;
    12.     case PG_CONTROL_LOOP:
    13.       p_actwk->patno = p_pg_data[0];
    14.       p_actwk->pattim = p_pg_data[1];
    15.       ret = 1;
    16.     case PG_CONTROL_END:
    17.       p_actwk->pattim = 0;
    18.       SPRITE_STATUS_PHANDLER(p_actwk, 52)(p_actwk);
    19.       ret = -1;
    20.   }
    21.  
    22.   patcnt = 0;
    23.   return ret;
    24. }
    While porting, I always make sure that the code is logically the same and in the same order as the original ASM, for authenticity's sake. But I may have to make some exceptions.
     
  18. BenoitRen

    BenoitRen

    Tech Member
    410
    183
    43
    This thread is starting to feel like a daily blog, with me being the only one posting.

    I've figured out what DAT_08fffaad is for! It contains the number of ghosts on the screen, with a maximum of 3 at the two darkest light levels.

    The same memory location is also used for something related to snow in Ice Cap, but I won't know what until I port that portion of the code.
     
  19. saxman

    saxman

    Oldbie Tech Member
    Don't worry about that. When I started hacking Sonic ROMs, few people really cared for the first 6 months to a year. But like my experience, with persistence, people can really come around to what you're doing in a big way. Seeing the results is sometimes what it takes to cement the big picture. And if you follow through with your work, I honestly believe this can have a huge impact in how some people will choose to make hacks/fan-games.

    I have nothing to really say about the finer details of the effort, and I suspect others feel the same. But keeping us up to date let's us know you're still working on it, and that's a good thing.
     
  20. aria

    aria

    growl from the digital haze Member
    182
    100
    43
    i saw soned2 supports chaotix five minutes ago
    fwiw, ive found it interesting reading this thread. i just don't really have a ton to add, as I have no experience with s3&k, or much ASM honestly. Still fascinating regardless, but I don't want to add pointless comments.