Sonic 1 Dynamic Pattern Load Cues for Objects

Discussion in 'Discussion and Q&A Archive' started by ProjectFM, Jun 9, 2015.

  1. ProjectFM

    ProjectFM Optimistic and self-dependent Member

    Joined:
    Oct 4, 2014
    Messages:
    912
    Location:
    Orono, Maine
    From my understanding, dynamic pattern load cues load different art tiles depending on the frame of an object, replacing the tiles for the previous frame. This allows only cartridge space to limit the amount of art tiles an object uses (besides the number of art tiles in one frame) instead of VRAM but you can't have something else on screen that uses the same art but a different frame because the overwritten tiles will make it look messed up. This is used for Sonic in Sonic 1, Sonic and Tails in Sonic 2, and Sonic, Tails, Knuckles, Shields, and other stuff in Sonic 3.

    In my Sonic 1 hack (Hivebrain), I want to use these for other objects (mainly shields and the giant ring) but I can't figure out how I can use it. Do you know a way I can get it to work or at least give me something I can work off of?

    I created a topic instead of posting in the Basic Q&A thread because there may be several replies and, if answered, will be easier to find by someone searching in the future.
     
  2. MarkeyJester

    MarkeyJester ♡ ! Member

    Joined:
    Jun 27, 2009
    Messages:
    2,867
    I'm not going to talk about how a Sonic engine would do it, or how you would do it using a subroutine or system in any Sonic game.  Instead, I'm going to explain how it would be done through any type of game working on the Mega Drive.  I believe, that by teaching you this way, would set you on the path to doing more extraordinary things, as opposed to just shield swapping or whatever...  Though I will keep it rather basic.

    The VRAM is 10000 bytes in size (0000 - FFFF), each 20 byte is considered a potential tile.  We'll write one tile of art data into VRAM from F300 - F31F (20 bytes).  The first thing to do, is set the VDP mode/address.  Two words are written to the VDP control port, here it is as a longword in binary:

    10NM LKJI HGFE DCBA ---- ---- 5432 --POThe numbers are the mode, here is a list:

    543210
    VRAM Read: 000000
    CRAM Read: 001000
    VSRAM Read: 000100
    VRAM Write: 000001
    CRAM Write: 000011
    VSRAM Write: 000101
    We want to write to VRAM, so VRAM Write mode is used:

    01NM LKJI HGFE DCBA ---- ---- 0000 --POThe starting address is F300, broken into binary that is 1111 0011 0000 0000.

    PONM LKJI HGFE DCBA
    F300: 1111 0011 0000 0000
    This binary pattern in set in place of the letters:

    0111 0011 0000 0000 ---- ---- 0000 --11The "-" symbols are invalid binary slots, effectively unused as far as we're aware, thus, they remain blank:

    0111 0011 0000 0000 0000 0000 0000 0011Converted to hexadecimal, this is 73000003, and is what is needed to be saved to the VDP's control port (C00004/C00006):

    move.l #$73000003,($C00004).lThe VDP is now in VRAM write mode.  Any data you write into the data port (C00000/C00002), will be copied into VRAM starting from F300 onwards:

    move.l #$11111111,($C00000).l
    Code:
    		move.l	#$11000011,($C00000).l
    Code:
    		move.l	#$10100101,($C00000).l
    Code:
    		move.l	#$10011001,($C00000).l
    Code:
    		move.l	#$10011001,($C00000).l
    Code:
    		move.l	#$10100101,($C00000).l
    Code:
    		move.l	#$11000011,($C00000).l
    Code:
    		move.l	#$11111111,($C00000).l
    That is one tile, a box with an X inside of it.  This is known as a manual transfer, the data is being sent manually (controlled by the 68k).

    The second known method is trough a "DMA" transfer (Direct Memory Addressing).  Where you supply the VDP the information about the data (where it is, how big it is, and where it's going to), and the VDP will stop the 68k, and transfer the data automatically and directly.  As an example, we'll pretend that our tile is in 68k RAM at 8000 - 801F, and we're transfering it to VRAM at F300 - F31F.

    The VDP has a series of mode registers, which can be altered to set a desired display mode (be it resolution, plane size, name table addressing, etc), but the registers we'll be interested in, are the DMA registers.  To write data to a VDP register, a word of data containing the register number and the byte to send to that register, are sent to the VDP control port (C00004/C00006):

    move.w #$8020,($C00004).lThe above example sends 20 to VDP register 80 (or register 00 if you prefer, the +80 is mearly a binary trigger).  The registers are from 80 to 97 (00 - 17).

    Registers 93 and 94 are the DMA size:

    move.l #$94009310,($C00004).lIn the above example, we're sending data to two registers 10 to register 93 and 00 to register 94.  The 00 10 together forms a word, this is the DMA size divided by 2 (0020 bytes / 2 = 0010), the division by two is due to an autoincrement register, but that's not important at this point in time.

    Registers 95, 96 and 97 are the DMA source:

    move.l #$96C09500,($C00004).l
    Code:
    		move.w	#$97[U]7F[/U],($C00004).l
    In the above example, we're sending data to three registers, 00 to register 95, C0 to register 96, and 7F to register 97.  The 7F C0 00 together forms a 24-bit address, divided by 2 (FF8000 / 2 = 7FC000).  68k address FF8000 is 68k RAM 8000.

    Now that the registers are set, the destination and mode needs to be set, this is the same as setting VRAM Write mode for manual transfer, but there are two differences:

    move.w #$7300,($C00004).l
    Code:
    		move.w	#$0083,($C00004).l
    The long-word being sent is 73000083.  This is similar to the 73000003 we sent for a manual transfer, except bit "5" of the binary mode is set to signify the address is for DMA transfer (this triggers DMA to start).  The second difference is that it's been split up and sent off a word at a time.  Where you write the first word doesn't matter, but the second word of the write address/mode MUST be written to C00004-C00005 instead of C00006-C00007.  Otherwise there's a strong posibility that the first word shall not be transfered.  As soon as that last word is written, the VDP will halt the 68k, and the 68k will not continue until the transfer is complete.
     
    Last edited by a moderator: Jun 9, 2015
  3. Pokepunch

    Pokepunch That guy who posts on occasion Member

    Joined:
    Aug 7, 2009
    Messages:
    270
    Location:
    UK
    For starters read through this thread, it proved a useful read when I strived for the same goal. Then you want to read through the S3K shield routines and see how they use PLCLoad_Shields, most of the lines related to it are commented.  
     
  4. ProjectFM

    ProjectFM Optimistic and self-dependent Member

    Joined:
    Oct 4, 2014
    Messages:
    912
    Location:
    Orono, Maine
    I found out that GreenSnake's multiple character guide replaced LoadSonicDynPLC with a system where there is a piece of code that loads the art, art location, and DPLC's and then branches to a piece of code that queues it and a piece of code that loads different art, mappings, DPLC's and animations depending on the character. I used this code to work with shields but it ended up messing up the game and the levels wouldn't even run unless I put the branch to the code that loads the DPLC's right after the beginning of the shield code. I also used the guide in the thread Pokepunch linked to. I've actually been unable to work with GreenSnake's guide when just using it for multiple characters no matter how many times I've tried to readd the code or what version of the guide I choose (I currently use the second one) because selecting a character other than Sonic yields me results similar to when I tried the shields. The problem may be because I use the 128 variation of the Hivebrain disassembly but I don't think so. Is there some way I can get it to work?

    Edit: The art isn't getting screwed up anymore because I removed an unnecessary branch to QueueDMATransfer. Unfortunately the shield art doesn't load and half the levels will infinitely reload when I enter them.

    Edit 2: I noticed that Sonic 3K splits each shield type into an object and saves the DPLC's in a similar way as mappings. I've decided to try that as apposed to using checks and tables and maybe use it for multiple characters too. I'll tell how it goes tomorrow (It's currently 11:18 at night and I have school tomorrow).
     
    Last edited by a moderator: Jun 10, 2015
  5. ProjectFM

    ProjectFM Optimistic and self-dependent Member

    Joined:
    Oct 4, 2014
    Messages:
    912
    Location:
    Orono, Maine
    So I decided that before I make a bunch of objects, I'll try to get the DPLC's working with just one object and one set of art, mappings, and animations. The game seems to run fine but the shields don't display any art.

    Here is the code for Obj38:

    ; ===========================================================================
    ; ---------------------------------------------------------------------------
    ; Object 38 - Shield
    ; ---------------------------------------------------------------------------

    Obj38: ; XREF: Obj_Index
    jmp LoadShieldDynPLC
    moveq #0,d0
    move.b $24(a0),d0
    move.w Shield_Index(pc,d0.w),d1
    jmp Shield_Index(pc,d1.w)
    ; ===========================================================================
    Shield_Index:
    dc.w Shield_Init-Shield_Index
    dc.w ShieldChecks-Shield_Index
    ; ===========================================================================
    Shield_Init:
    addq.b #2,$24(a0)
    move.l #Map_lshield,4(a0) ; Load Shield Map into place
    move.b #4,1(a0)
    move.b #1,$18(a0)
    move.b #$18,$19(a0)
    move.w #$541,2(a0) ; Set VRAM location
    btst #7,($FFFFD002).w
    beq.s ShieldChecks
    bset #7,2(a0)
    ; ---------------------------------------------------------------------------

    ShieldChecks:
    tst.b ($FFFFFE2C).w ; Test if Sonic has a shield
    bne.s SonicHasShield ; If so, branch to do nothing
    tst.b ($FFFFFE2D).w ; Test if Sonic got invisibility
    beq.s jmp_Deleteobj38 ; If so, delete object temporarily

    ShieldProperties:
    move.w ($FFFFD008).w,8(a0) ; Load Main Character X-position
    move.w ($FFFFD00C).w,$C(a0) ; Load Main Character Y-position
    ; move.b ($FFFFD022).w,$22(a0) ; Something about Character status
    lea (Ani_obj38).l,a1
    jsr AnimateSprite
    jmp DisplaySprite

    SonicHasShield:
    rts

    jmp_DeleteObj38: ; loc_12648:
    jmp DeleteObject
    Here's the code for loading DPLC's

    Code:
    LoadShieldDynPLC:
    		movea.l	(LShieldDynPLC).l,a2	; get DPLC location
    		move.w	#$A820,d4		; offset in VRAM to store art
    		move.l	(UnC_LightningShield).l,d6	; get art location
    		
    		moveq	#0,d0
    		move.b	$1A(a0),d0	; load frame number
    		cmp.b	$FFFFF601.w,d0	; check if equal with last queued frame
    		beq.s	DPLC_End	; if is, don't load new DPLC
    		move.b	d0,$FFFFF601.w	; remember queued frame
    ; End of function LoadShieldDynPLC
    
    ; ---------------------------------------------------------------------------
    ; Subroutine to queue any pattern load cue
    ; Input: a2 - DPLC file, d4 - VRAM address, d6 - Art file, d0 - frame number
    ; ---------------------------------------------------------------------------
    
    Load_DPLC:
    		add.w	d0,d0		; multiply by 2
    		adda.w	(a2,d0.w),a2	; get the right DPLC location
    		moveq	#0,d5		; quckly clear d5
    		move.b	(a2)+,d5	; then move the amount of requests to d5
    		subq.w	#1,d5		; subtract 1
    		bmi.s	DPLC_End	; if negative, branch away
    
    DPLC_ReadEntry:
    		moveq	#0,d1
    		move.b	(a2)+,d1	; get first byte to d1, and increment pointer
    		lsl.w	#8,d1		; shift 8 bits left
    		move.b	(a2)+,d1 	; move second byte to d1
    
    		move.w	d1,d3		; move d1 to d3
    		lsr.w	#8,d3		; shift 8 bits right
    		andi.w	#$F0,d3		; leave only bits 7, 6, 5, and 4
    		addi.w	#$10,d3		; add $10 to d3
    
    		andi.w	#$FFF,d1	; filter out bits 15, 14, 13 and 12
    		lsl.l	#5,d1		; shift 5 bits left
    		add.l	d6,d1		; add the art address to d1
    		move.w	d4,d2		; move VRAM location to d2
    		add.w	d3,d4		; add d3 to VRAM address
    		add.w	d3,d4		; add d3 to VRAM address
    
    		jsr	QueueDMATransfer; Save it to the DMA queue
    		dbf	d5,DPLC_ReadEntry; repeat for number of requests
    
    DPLC_End:
    		rts			; return
     
    Last edited by a moderator: Jun 10, 2015
  6. ProjectFM

    ProjectFM Optimistic and self-dependent Member

    Joined:
    Oct 4, 2014
    Messages:
    912
    Location:
    Orono, Maine
    Woops! Accidentally created a blank post when editing the other one.

     
     
    Last edited by a moderator: Jun 10, 2015
  7. Pokepunch

    Pokepunch That guy who posts on occasion Member

    Joined:
    Aug 7, 2009
    Messages:
    270
    Location:
    UK
    I'm not sure if it makes any difference, but I'd personally place the branch to the DPLC code just before display sprite. Having it before everything means it will run without the necessary information for the first frame, seems kinda pointless. 

    Also all of my shields are one object, so it's possible albeit a tad messy. 
     
    Last edited by a moderator: Jun 10, 2015
  8. ProjectFM

    ProjectFM Optimistic and self-dependent Member

    Joined:
    Oct 4, 2014
    Messages:
    912
    Location:
    Orono, Maine
    ​Doing that will make the game crash when the level starts. It has to be at the beginning unless someone can figure out how to keep it from crashing.
     
  9. AURORA☆FIELDS

    AURORA☆FIELDS so uh yes Exiled

    Joined:
    Oct 7, 2011
    Messages:
    759
    I am not sure if this is the actual problem, but this is certainly wrong: 

    movea.l (LShieldDynPLC).l,a2 ; get DPLC location
    move.w #$A820,d4 ; offset in VRAM to store art
    move.l (UnC_LightningShield).l,d6 ; get art location

    it should be as follows:

    lea (LShieldDynPLC).l,a2 ; get DPLC location
    move
    .w #$A820,d4 ; offset in VRAM to store art
    move
    .l #UnC_LightningShield,d6 ; get art location

    The thing is, unlike on Sonic object, we don't get the address from RAM, but instead constants, and we need to slightly alter how its loaded (and this slight alteration is vital). Another thing I suggest is using object SST for checking if the frame is loaded (instead of static RAM address). I personally use $3A but its up to you. And in fact the branch to DPLC loading is often most useful after AnimateSprite and before DisplaySprite.

    If you still have problems, let me know by messaging me and I see what I can do
     
  10. ProjectFM

    ProjectFM Optimistic and self-dependent Member

    Joined:
    Oct 4, 2014
    Messages:
    912
    Location:
    Orono, Maine
    Attached is a clean disassembly with the code and files I used. The shield still doesn't show up but it allows you to play levels fine without the jump to LoadShieldDynPLC at the beginning. The invincibility stars are messed up but thats because added art from my own hack that doesn't work with the mappings and just added it for the sake of having uncompressed invincibility stars art. There is also art and mappings and stuff for the bubble and flame shield but they aren't used and should not be bothered with (the DPLC's are also blank). I'd appreciate it if you could look over it and find an flaws that I may have missed.

    View attachment Sonic128test.zip
     
  11. Irixion

    Irixion Well-Known Member Member

    Joined:
    Aug 11, 2007
    Messages:
    670
    Location:
    Ontario, Canada
    You know, if I remember correctly, the spindash dush uses DPLCs, and the spindash dust is an object, like I said, iirc. So you could look at that.  But it's Sonic 2's code, and that's...yeah that's just blah.
     
  12. AURORA☆FIELDS

    AURORA☆FIELDS so uh yes Exiled

    Joined:
    Oct 7, 2011
    Messages:
    759
    I think I found out the issue, and there is 2 of them:

    lea (Ani_obj38).l,a1
    jsr AnimateSprite
    jmp LoadShieldDynPLC ; <- ???
    jmp DisplaySprite ; this is never reached
    Another was your DPLC is still incorrect, just switching it with one from my disassembly fixed the graphical problems of the shield and in fact it seems to work just about fine!
     
  13. ProjectFM

    ProjectFM Optimistic and self-dependent Member

    Joined:
    Oct 4, 2014
    Messages:
    912
    Location:
    Orono, Maine
    I fixed the DPLC file (it turns out that it uses player DPLC's instead of object DPLC's) but I still don't understand why display sprite is never gotten to and what I can do to fix it.
     
  14. AURORA☆FIELDS

    AURORA☆FIELDS so uh yes Exiled

    Joined:
    Oct 7, 2011
    Messages:
    759
    its simply because you just the "jmp" instruction instead of "jsr" on "LoadShieldDynPLC". JMP simply JuMP's to address, meaning it will never return, whereas "JSR" goes to a subroutine, and starts execution on the very next instruction. I recommend reading MarkeyJester's 68k tutorial, specifically about JMP and JSR.
     
    Last edited by a moderator: Jun 11, 2015
  15. ProjectFM

    ProjectFM Optimistic and self-dependent Member

    Joined:
    Oct 4, 2014
    Messages:
    912
    Location:
    Orono, Maine
    Awesome! Thanks GreenSnake.

    Edit: The problem where the game crashed unless the code for the shield is blank (or if it branched to somewhere different where it wouldn't return was caused by a very stupid branch to the code that I added in the level loading routine.
     
    Last edited by a moderator: Jun 11, 2015