Friday, November 3, 2017

DPLC fade bug

There's a bug involving DPLCs and the fade effect that occurs when you transition from one stage to the next. When a DPLC request is performed on the same exact frame as the fade out, the new art will be DMA'd to VRAM, but the new sprite mappings won't, giving the affected objects a corrupted appearance.


This bug usually manifests itself in the player's barrier when entering a bonus stage: barriers animate once every other frame, meaning there's a 50% chance that the art will be requested on the same frame as the fade out. Meanwhile, the player is usually in their rolling animation, where all the sprites have the same basic square shape anyway, so they get away unscathed.


The reason for the bug is as follows: once every frame, two sibling functions are executed, these are Process_Sprites and Render_Sprites. The former, as we've previously seen, yields control to each object in RAM, allowing them to run their own code. The latter goes though each object's SST and copies all the necessary attributes to VRAM so the VDP can render them as sprites.

To request a level change, a RAM flag should be set; this is called the Restart_level_flag in the current disassembly. If this flag is detected, control jumps from the main level loop back to the code which starts a level from scratch.
LevelLoop:
   bsr.w   Pause_Game
   move.b  #8,(V_int_routine).w
   jsr     (Process_Kos_Queue).l
   bsr.w   Wait_VSync
   addq.w  #1,(Level_frame_counter).w
   bsr.w   Demo_PlayRecord
   jsr     (Animate_Palette).l
   jsr     (SpecialEvents).l
   jsr     (Load_Sprites).l
   jsr     (Process_Sprites).l
   tst.w   (Restart_level_flag).w
   bne.w   Level
   jsr     (DeformBgLayer).l
   jsr     (ScreenEvents).l
   bsr.w   Handle_Onscreen_Water_Height
   jsr     (Load_Rings).l
   cmpi.b  #9,(Current_zone).w
   bne.s   Level_NotLRZ
   jsr     (Draw_LRZ_Special_Rock_Sprites).l

Level_NotLRZ:
    jsr     (Animate_Tiles).l
    bsr.w   Process_Nem_Queue_Init
    jsr     (Process_Kos_Module_Queue).l
    bsr.w   sub_773C
    bsr.w   sub_77D2
    jsr     (Render_Sprites).l
    ...
However, due to the way the code is structured, when the Restart_level_flag is set, we enter a situation where there is a call to Process_Sprites with no matching call to Render_Sprites.

As we saw before, objects perform DPLC requests by calling a function such as Perform_DPLC from within their code. When Process_Sprites runs, it will process all active objects, including their DPLC requests. If the Restart_level_flag is set however, Render_Sprites never runs, so the new mappings are never sent to the VDP, causing a mismatch.

The code is structured like this for a reason. When the star post object sets the Restart_level_flag, it also changes the current zone to the bonus stage. That way, once the level starts over from scratch, the bonus stage will load, instead of the level the player is already in.
loc_2D4CA:
    move.w  d1,(Current_zone_and_act).w
    move.w  d1,(Apparent_zone_and_act).w
    move.w  (Ring_count).w,(Saved_ring_count).w
    move.b  (Extra_life_flags).w,(Saved_extra_life_flags).w
    move.b  #0,(Last_star_post_hit).w
    move.b  #1,(Restart_level_flag).w
    ...
This means that, for all intents and purposes, when Process_Sprites returns, the game has already entered the bonus stage, so if the the other functions were allowed to run, they would will behave as such: Animate_Palette would do the cycling palettes for the bonus stage, DeformBgLayer would scroll the background layer as the bonus stage would, and Render_Sprites would render the smaller version of the HUD. This would fix the DPLC bug, but at what cost?


The correct solution is to clear the DMA queue at the start of the level loop, just like the KosM and Nemesis queues do.

No comments:

Post a Comment