Skip to content

Instantly share code, notes, and snippets.

@Pokechu22
Last active May 17, 2022 20:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Pokechu22/5f83afb548bef8d75d3575d1c02bd518 to your computer and use it in GitHub Desktop.
Save Pokechu22/5f83afb548bef8d75d3575d1c02bd518 to your computer and use it in GitHub Desktop.
Messy Major Minor's Majestic March notes

Major minor: at one point in the tutorial, the screen does get greyed out, even with Dolphin's broken rendering. That's a starting point to figure out why things broke.

EID 611 - the scissor region is over the screen in that case, but not in the others. Obj 232, which renders:

BP register BPMEM_SCISSOROFFSET
Scissor X offset: 171
Scissor Y offset: 171

Object 231, which doesn't render (actually in 218):

BP register BPMEM_SCISSOROFFSET
Scissor X offset: -341
Scissor Y offset: 171

From the original cutscene fifolog, object 1:

BP register BPMEM_SCISSOROFFSET
Scissor X offset: 171
Scissor Y offset: -341
void gx::GXSetScissorBoxOffset(int param_1,int param_2)

{
  write_volatile_1(DAT_cc008000,0x61);
  write_volatile_4(0xcc008000,
                   (param_2 + 0x156) * 0x200 & 0xffc00U | param_1 + 0x156U >> 1 & 0x3ff | 0x59000000
                  );
  *(undefined2 *)(DAT_80361b50 + 2) = 0;
  return;
}

0x156 is 342.

So calling with (0, 0) gives 342, 342. But they get divided by 2, so it actually gives (171, 171). OK.

The loading screen adjusts the scissor offset to scroll. OK.

80065ac0 GameLayout_setScissor Easy enough.

Dynamicly allocated: Gamelayout struct is at 806fd260. Relevant fields are at 806fd2a4, 806fd2a8. There are 4 sprite managers, at 805a7e00/805ada80/805b9300/805bef80. Relevant fields have offset 0x38/0x3c (giving 0xb8/0xbc for the ones ending in 0x80).

Messy memory breakpoint output, cleaned up later on

Init:

21:26:177 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065484 (GameLayout_init) Write32 0 at 806fd2a4 ( --- )
21:26:177 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065488 (GameLayout_init) Write32 0 at 806fd2a8 ( --- )

Press a on title:

21:40:810 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 3e0 at 806fd2a4 ( --- )
21:40:810 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:40:836 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 3c0 at 806fd2a4 ( --- )
21:40:836 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:40:870 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 3a0 at 806fd2a4 ( --- )
21:40:870 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:40:903 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 380 at 806fd2a4 ( --- )
21:40:903 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:40:936 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 360 at 806fd2a4 ( --- )
21:40:936 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:40:967 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 340 at 806fd2a4 ( --- )
21:40:967 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:003 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 320 at 806fd2a4 ( --- )
21:41:003 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:034 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 300 at 806fd2a4 ( --- )
21:41:034 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:069 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 2e0 at 806fd2a4 ( --- )
21:41:069 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:105 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 2c0 at 806fd2a4 ( --- )
21:41:105 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:134 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 2a0 at 806fd2a4 ( --- )
21:41:135 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:171 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 280 at 806fd2a4 ( --- )
21:41:171 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:202 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 260 at 806fd2a4 ( --- )
21:41:202 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:237 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 240 at 806fd2a4 ( --- )
21:41:237 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:268 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 220 at 806fd2a4 ( --- )
21:41:268 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:303 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 200 at 806fd2a4 ( --- )
21:41:303 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:335 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 1e0 at 806fd2a4 ( --- )
21:41:335 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:371 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 1c0 at 806fd2a4 ( --- )
21:41:371 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:400 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 1a0 at 806fd2a4 ( --- )
21:41:400 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:41:438 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 180 at 806fd2a4 ( --- )
21:41:438 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )

Finishes loading main menu:

21:43:639 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 660 at 806fd2a4 ( --- )
21:43:639 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:43:673 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 640 at 806fd2a4 ( --- )
21:43:673 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:43:704 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 620 at 806fd2a4 ( --- )
21:43:704 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:43:736 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 600 at 806fd2a4 ( --- )
21:43:736 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:43:771 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 5e0 at 806fd2a4 ( --- )
21:43:771 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:43:803 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 5c0 at 806fd2a4 ( --- )
21:43:804 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:43:840 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 5a0 at 806fd2a4 ( --- )
21:43:840 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:43:874 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 580 at 806fd2a4 ( --- )
21:43:874 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:43:906 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 560 at 806fd2a4 ( --- )
21:43:906 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:43:940 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 540 at 806fd2a4 ( --- )
21:43:941 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:43:971 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 520 at 806fd2a4 ( --- )
21:43:971 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:44:004 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 500 at 806fd2a4 ( --- )
21:44:004 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:44:036 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 4e0 at 806fd2a4 ( --- )
21:44:036 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:44:071 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 4c0 at 806fd2a4 ( --- )
21:44:071 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:44:112 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 4a0 at 806fd2a4 ( --- )
21:44:112 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:44:140 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 480 at 806fd2a4 ( --- )
21:44:140 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:44:173 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 460 at 806fd2a4 ( --- )
21:44:173 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:44:205 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 440 at 806fd2a4 ( --- )
21:44:205 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:44:239 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 420 at 806fd2a4 ( --- )
21:44:239 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )
21:44:271 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 400 at 806fd2a4 ( --- )
21:44:271 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 0 at 806fd2a8 ( --- )

Click return to title: starts scrolling up

21:49:412 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:412 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 3ee at 806fd2a8 ( --- )
21:49:443 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:443 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 3dc at 806fd2a8 ( --- )
21:49:478 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:478 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 3ca at 806fd2a8 ( --- )
21:49:510 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:510 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 3b8 at 806fd2a8 ( --- )
21:49:545 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:545 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 3a6 at 806fd2a8 ( --- )
21:49:578 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:578 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 394 at 806fd2a8 ( --- )
21:49:610 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:610 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 382 at 806fd2a8 ( --- )
21:49:645 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:645 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 370 at 806fd2a8 ( --- )
21:49:676 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:676 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 35e at 806fd2a8 ( --- )
21:49:710 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:710 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 34c at 806fd2a8 ( --- )
21:49:747 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:747 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 33a at 806fd2a8 ( --- )
21:49:776 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:776 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 328 at 806fd2a8 ( --- )
21:49:812 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:812 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 316 at 806fd2a8 ( --- )
21:49:845 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:845 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 304 at 806fd2a8 ( --- )
21:49:877 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:877 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 2f2 at 806fd2a8 ( --- )
21:49:910 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:911 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 2e0 at 806fd2a8 ( --- )
21:49:943 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:943 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 2ce at 806fd2a8 ( --- )
21:49:980 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:49:980 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 2bc at 806fd2a8 ( --- )
21:50:012 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:50:012 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 2aa at 806fd2a8 ( --- )
21:50:045 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:50:045 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 298 at 806fd2a8 ( --- )
21:50:079 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:50:079 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 286 at 806fd2a8 ( --- )
21:50:110 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:50:110 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 274 at 806fd2a8 ( --- )
21:50:144 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:50:144 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 262 at 806fd2a8 ( --- )
21:50:177 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:50:177 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 250 at 806fd2a8 ( --- )
21:50:210 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:50:210 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 23e at 806fd2a8 ( --- )
21:50:247 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:50:247 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 22c at 806fd2a8 ( --- )
21:50:279 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:50:279 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 220 at 806fd2a8 ( --- )

Finishes loading, more scrolling:

21:51:029 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:029 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 5ce at 806fd2a8 ( --- )
21:51:064 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:064 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 5bc at 806fd2a8 ( --- )
21:51:095 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:095 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 5aa at 806fd2a8 ( --- )
21:51:131 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:131 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 598 at 806fd2a8 ( --- )
21:51:160 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:160 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 586 at 806fd2a8 ( --- )
21:51:197 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:197 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 574 at 806fd2a8 ( --- )
21:51:228 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:228 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 562 at 806fd2a8 ( --- )
21:51:262 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:262 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 550 at 806fd2a8 ( --- )
21:51:295 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:295 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 53e at 806fd2a8 ( --- )
21:51:329 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:329 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 52c at 806fd2a8 ( --- )
21:51:360 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:360 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 51a at 806fd2a8 ( --- )
21:51:395 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:395 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 508 at 806fd2a8 ( --- )
21:51:431 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:431 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 4f6 at 806fd2a8 ( --- )
21:51:461 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:461 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 4e4 at 806fd2a8 ( --- )
21:51:498 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:498 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 4d2 at 806fd2a8 ( --- )
21:51:528 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:528 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 4c0 at 806fd2a8 ( --- )
21:51:564 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:564 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 4ae at 806fd2a8 ( --- )
21:51:595 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:595 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 49c at 806fd2a8 ( --- )
21:51:629 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:629 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 48a at 806fd2a8 ( --- )
21:51:661 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:661 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 478 at 806fd2a8 ( --- )
21:51:698 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:698 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 466 at 806fd2a8 ( --- )
21:51:729 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:729 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 454 at 806fd2a8 ( --- )
21:51:764 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:764 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 442 at 806fd2a8 ( --- )
21:51:795 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:795 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 430 at 806fd2a8 ( --- )
21:51:831 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:831 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 41e at 806fd2a8 ( --- )
21:51:861 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:861 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 40c at 806fd2a8 ( --- )
21:51:898 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac0 (GameLayout_setScissor) Write32 0 at 806fd2a4 ( --- )
21:51:898 Core\PowerPC\BreakPoints.cpp:308 N[MI]: MBP 80065ac4 (GameLayout_setScissor) Write32 400 at 806fd2a8 ( --- )

After doing this, the title screen is white.

In fact, when pressing A on the title screen, it IMMEDIATELY goes blank, instead of scrolling. Huh.

Hardware: https://youtu.be/9apb8K5tWLc?t=40
Dolphin: https://youtu.be/nrl0NFycXOw?t=20

A very simple patch is this:

diff --git a/Source/Core/VideoCommon/BPStructs.cpp b/Source/Core/VideoCommon/BPStructs.cpp
index 593417d485..c1f5347b59 100644
--- a/Source/Core/VideoCommon/BPStructs.cpp
+++ b/Source/Core/VideoCommon/BPStructs.cpp
@@ -131,6 +131,8 @@ static void BPWritten(const BPCmd& bp)
   case BPMEM_SCISSORTL:      // Scissor Rectable Top, Left
   case BPMEM_SCISSORBR:      // Scissor Rectable Bottom, Right
   case BPMEM_SCISSOROFFSET:  // Scissor Offset
+    bpmem.scissorOffset.x = 171;
+    bpmem.scissorOffset.y = 171;
     SetScissor();
     SetViewport();
     VertexShaderManager::SetViewportChanged();

While this gets things to render, the scrolling breaks. It also breaks the boss roars in Super Mario Galaxy... which might be a good thing to analyze too:

Super Mario Galaxy, frame 2: objects 604 and 605. But object 603 is what actually adjusts the scissor rectangle.

BP register BPMEM_SCISSOROFFSET (603)
Scissor X offset: 171
Scissor Y offset: -61

BP register BPMEM_SCISSOROFFSET (604)
Scissor X offset: 171
Scissor Y offset: 171

OK, these numbers are hard to visualize. I've converted them back to the ranges that would be passed as inputs to GXSetScissorBoxOffset:

BP register BPMEM_SCISSOROFFSET (603)
Scissor X offset: 0 (171)
Scissor Y offset: -464 (-61)

BP register BPMEM_SCISSOROFFSET (604)
Scissor X offset: 0 (171)
Scissor Y offset: 0 (171)

This still violates the warning that the x value should be between -342 - 382 inclusive and the y value should be between -342 - 494 inclusive. But that's fine.

And also the values for major minor:

BP register BPMEM_SCISSOROFFSET (232)
Scissor X offset: 0 (171)
Scissor Y offset: 0 (171)

BP register BPMEM_SCISSOROFFSET (231 / 218)
Scissor X offset: -1024 (-341)
Scissor Y offset: 0 (171)

BP register BPMEM_SCISSOROFFSET (orig 1)
Scissor X offset: 0 (171)
Scissor Y offset: -1024 (-341)

Next idea for a fix:

diff --git a/Source/Core/VideoCommon/BPStructs.cpp b/Source/Core/VideoCommon/BPStructs.cpp
index 593417d485..c1f5347b59 100644
--- a/Source/Core/VideoCommon/BPStructs.cpp
+++ b/Source/Core/VideoCommon/BPStructs.cpp
@@ -131,6 +131,8 @@ static void BPWritten(const BPCmd& bp)
   case BPMEM_SCISSORTL:      // Scissor Rectable Top, Left
   case BPMEM_SCISSORBR:      // Scissor Rectable Bottom, Right
   case BPMEM_SCISSOROFFSET:  // Scissor Offset
+    bpmem.scissorOffset.x = ((((bpmem.scissorOffset.x << 1) - 342) & (1024 - 1)) + 342) >> 1;
+    bpmem.scissorOffset.y = ((((bpmem.scissorOffset.y << 1) - 342) & (1024 - 1)) + 342) >> 1;
     SetScissor();
     SetViewport();
     VertexShaderManager::SetViewportChanged();

And yes, this gets things to render, but the title screen is still broken (it immediately disappears instead of scrolling), and this still breaks the boss roar. Probably because it's handling negative values wrong still.

SceneLoading_tick (8005f920) does deliberately add 1024 in some cases (see FLOAT_80361108 = 1024.0)
Note that for SceneLoading_tick to decompile right, 8000fb78 must have a signature of int zz_8000fb78_(void) and 8000fb90 must have a signature of bool get_8023e869(void) (the automatic name for that function is very misleading).
It's still hard to understand what it's doing, because ghidra and floats don't go well together, and because of C++ vtables. But it's clear that the 1024 is deliberate. (For reference, 8005fde4 is a call to SceneLoading_preparescissor at 80060294).

Because the above pile of output is a bit confusing, let's focus on one value at a time.

Memory breakpoint at 806fd2a4 (gameManager->gameLayout->scissorXOff, dynamically allocated but seems to be consistent).

First, we get these (decimal followed by hex) as the title screen goes away:

992, 960, 928, 896, 864, 832, 800, 768, 736, 704, 672, 640, 608, 576, 544, 512, 480, 448, 416, 384
3e0, 3c0, 3a0, 380, 360, 340, 320, 300, 2e0, 2c0, 2a0, 280, 260, 240, 220, 200, 1e0, 1c0, 1a0, 180

Then we get these as the menu screen loads in, with the 3rd value being sign extension (computed by subtracting 2048 / 0x800):

1632, 1600, 1568, 1536, 1504, 1472, 1440, 1408, 1376, 1344, 1312, 1280, 1248, 1216, 1184, 1152, 1120, 1088, 1056,  1024
 660,  640,  620,  600,  5e0,  5c0,  5a0,  580,  560,  540,  520,  500,  4e0,  4c0,  4a0,  480,  460,  440,  420,   400
-416, -448, -480, -512, -544, -576, -608, -640, -672, -704, -736, -768, -800, -832, -864, -896, -928, -960, -992, -1024

And when going back to the title screen from the menu, with a breakpoint at 806fd2a8 (gameManager->gameLayout->scissorYOff):

First, we get these (decimal followed by hex) as the menu screen goes away:

1006, 988, 970, 952, 934, 916, 898, 880, 862, 844, 826, 808, 790, 772, 754, 736, 718, 700, 682, 664, 646, 628, 610, 592, 574, 556, 544
3ee,  3dc, 3ca, 3b8, 3a6, 394, 382, 370, 35e, 34c, 33a, 328, 316, 304, 2f2, 2e0, 2ce, 2bc, 2aa, 298, 286, 274, 262, 250, 23e, 22c, 220

Then we get these as the title screen loads in:

1486, 1468, 1450, 1432, 1414, 1396, 1378, 1360, 1342, 1324, 1306, 1288, 1270, 1252, 1234, 1216, 1198, 1180, 1162, 1144, 1126, 1108, 1090, 1072, 1054,  1036,  1024
 5ce,  5bc,  5aa,  598,  586,  574,  562,  550,  53e,  52c,  51a,  508,  4f6,  4e4,  4d2,  4c0,  4ae,  49c,  48a,  478,  466,  454,  442,  430,  41e,   40c,   400
-562, -580, -598, -616, -634, -652, -670, -688, -706, -724, -742, -760, -778, -796, -814, -832, -850, -868, -886, -904, -922, -940, -958, -976, -994, -1012, -1024

The negative values are an issue, but the title screen immediately disappearing indicates that they aren't the only problem.

I notice that the difference between values for x is 32 / 0x20, and for y it is 18 / 0x12 apart from the last one where it is 12/0xc. Note also that 384 = 1024 - 640, 1632 = 1024 + 640 - 20, 544 = 1024 - 480, and 1486 = 1024 + 480 - 18.

How about remapping it like this:

-1024  -513  -512    -1      0   511   512  1023
    0   511  -512    -1      0   511  -512    -1

First, to make things easier, let's rework the way we're editing the value:

diff --git a/Source/Core/VideoCommon/BPStructs.cpp b/Source/Core/VideoCommon/BPStructs.cpp
index 593417d485..598174eb2d 100644
--- a/Source/Core/VideoCommon/BPStructs.cpp
+++ b/Source/Core/VideoCommon/BPStructs.cpp
@@ -131,11 +131,21 @@ static void BPWritten(const BPCmd& bp)
   case BPMEM_SCISSORTL:      // Scissor Rectable Top, Left
   case BPMEM_SCISSORBR:      // Scissor Rectable Bottom, Right
   case BPMEM_SCISSOROFFSET:  // Scissor Offset
+  {
+    auto converttoinput = [](int val) { return (val << 1) - 342; };
+    auto remap = [](int val) {
+      // This is still the old incorrect conversion
+      return val & (1024 - 1);
+    };
+    auto converttoreg = [](int val) { return (val + 342) >> 1; };
+    bpmem.scissorOffset.x = converttoreg(remap(converttoinput(bpmem.scissorOffset.x)));
+    bpmem.scissorOffset.y = converttoreg(remap(converttoinput(bpmem.scissorOffset.y)));
     SetScissor();
     SetViewport();
     VertexShaderManager::SetViewportChanged();
     GeometryShaderManager::SetViewportChanged();
     return;
+  }
   case BPMEM_LINEPTWIDTH:  // Line Width
     GeometryShaderManager::SetLinePtWidthChanged();
     return;

And now adjust it to implement that logic in a very simple way:

diff --git a/Source/Core/VideoCommon/BPStructs.cpp b/Source/Core/VideoCommon/BPStructs.cpp
index 593417d485..9153884657 100644
--- a/Source/Core/VideoCommon/BPStructs.cpp
+++ b/Source/Core/VideoCommon/BPStructs.cpp
@@ -131,11 +131,24 @@ static void BPWritten(const BPCmd& bp)
   case BPMEM_SCISSORTL:      // Scissor Rectable Top, Left
   case BPMEM_SCISSORBR:      // Scissor Rectable Bottom, Right
   case BPMEM_SCISSOROFFSET:  // Scissor Offset
+  {
+    auto converttoinput = [](int val) { return (val << 1) - 342; };
+    auto remap = [](int val) {
+      if (val < -512)
+        val += 1024;
+      else if (val > 511)
+        val -= 1024;
+      return val;
+    };
+    auto converttoreg = [](int val) { return (val + 342) >> 1; };
+    bpmem.scissorOffset.x = converttoreg(remap(converttoinput(bpmem.scissorOffset.x)));
+    bpmem.scissorOffset.y = converttoreg(remap(converttoinput(bpmem.scissorOffset.y)));
     SetScissor();
     SetViewport();
     VertexShaderManager::SetViewportChanged();
     GeometryShaderManager::SetViewportChanged();
     return;
+  }
   case BPMEM_LINEPTWIDTH:  // Line Width
     GeometryShaderManager::SetLinePtWidthChanged();
     return;

This actually fixes it! Or at least the title screen is fixed. I still see a flash of white after loading screens though.

In major_minor_loading_long.dff (120 frames), frame 46 is the last loading frame, 47 is when the scroll starts, frame 50 is the last all-white frame, and frame 51 is WEIRD (even if it's the only enabled frame, you get multiple outputs?) - though it works fine with immediately present XFB enabled. Frames 51+ have the world visible.

On frame 49:

BP register BPMEM_SCISSOROFFSET
Scissor X offset: -480 (-69)
Scissor Y offset: 0 (171)

which stays at -480...

On frame 50:

BP register BPMEM_SCISSOROFFSET
Scissor X offset: -512 (-85)
Scissor Y offset: 0 (171)

which stays as -512...

And on frame 51:

BP register BPMEM_SCISSOROFFSET
Scissor X offset: -544 (-101)
Scissor Y offset: 0 (171)

which becomes +480. This is definitely wrong, looking at renderdoc.

I think I need a better way to visualize this - renderdoc helps, but it doesn't show ones that end up completely off-screen. Interestingly, there's existing imgui functionality that helps - OverlayProjStats under [Settings] in GFX.ini (not exposed in the UI). This doesn't really work that well, since it assumes the projection is the same for the whole frame, but it's a start...


I've done some experimentation with additional rendering with imgui, but haven't figured out a proper fix yet. It does help visualize what's going on, though. One thing I have found is that getting rid of the game's 1024 offset does fix everything (write 00000000 to 80361108), but it should be possible to emulate correctly without a patch.

OK, this makes perfect sense. The jump from one side to the other I saw before actually happens on console... sorta. My hardware test: https://youtu.be/2I6ODJzUVZg

The key take-away here is that in some cases, the scissor region is on both sides of the screen, and this happens twice (so 1024 and 0 are the same). This probably explains the range given in the comment on GX_SetScissorBoxOffset: x between -342 and 382 inclusive and y between -342 and 494 inclusive. The negative values are because of when things wrap around (internally 382 is added to the registers), but aren't actually an issue in practice (per Super Mario Galaxy), and probably can instead be -382 or -494 instead. Those values make sense when looking at the EFB size, which is 640 by 528: observe that 640 + 382 = 528 + 494 = 1022. (1022 is used instead of 1024 because if you use 1024 then it's equal, and it works in increments of 2... I think? This is something that could be hardware tested too.)

I augmented the hardware test to print out some additional information about when pixels at the corners of the screen change colors.

Raw data

Note that this is only when pixels change to or from 000000, not all changes (as otherwise there would be a change every frame).

Results for pixel 0 0:
Offset 0 0: Changed 000000 -> 00fefe
Offset 640 0: Changed fefffe -> 000000
Offset 1024 0: Changed 000000 -> 00fefe
Offset 1664 0: Changed fefffe -> 000000
Offset 0 0: Changed 000000 -> 00fefe
Offset 0 480: Changed fd00fe -> 000000
Offset 0 1024: Changed 000000 -> 00fefe
Offset 0 1504: Changed fd00fe -> 000000

Results for pixel 639 0:
Offset 0 0: Chaned 000000 -> fefffe
Offset 2 0: Changed fefffe -> 000000
Offset 386 0: Changed 000000 -> 00fffe
Offset 1026 0: Changed fefffe -> 000000
Offset 1410 0: Changed 000000 -> 00fffe
Offset 0 480: Changed feff00 -> 000000
Offset 0 1024: Changed 000000 -> fefffe
Offset 0 1504: Changed feff00 -> 000000

Results for pixel 0 479:
Offset 0 0: Changed 000000 -> fd00fe
Offset 640 0: Changed fdfe00 -> 000000
Offset 1024 0: Changed 000000 -> fd00fe
Offset 1664 0: Changed fdfe00 -> 000000
Offset 0 0: Changed 000000 -> fd00fe
Offset 0 2: Changed fd00fe -> 000000
Offset 0 546: Changed 000000 -> 00fdfe
Offset 0 1026: Changed fd00fe -> 000000
Offset 0 1570: Changed 000000 -> 00fdfe

Results for pixel 639 479:
Offset 0 0: Changed 000000 -> fdff00
Offset 2 0: Changed fdff00 -> 000000
Offset 386 0: Changed 000000 -> fd00fd
Offset 1026 0: Changed fdff00 -> 000000
Offset 1410 0: Changed 000000 -> fd00fd
Offset 0 2: Changed fdff00 -> 000000
Offset 0 546: Changed 000000 -> fdfffd
Offset 0 1026: Changed fdff00 -> 000000
Offset 0 1570: Changed 000000 -> fdfffd

Some properties (this is with a 640 by 480 viewport - different things would be used for a different size):

For x = 0, the pixel is visible for [0, 640) and [1024, 1664) ([-1024, -384)). For x = 639, the pixel is visible for {0}, [386, 1026), and [1410, 2048) (also interpretable as {0}, [386, 1024), {1024}, and [1410, 2048) ({-1024} and [-638, 0))). These intervals overlap at {0}, [386, 640), {1024}, and [1410, 1664) ({-1024} and [-638, -384)), and only {0} and {1024} are overlaps where the results make sense (cases where the whole screen is visible), giving safe values of [0, 386), [640, 1024), [1024, 1410), and [1664, 2048) (or [-1024, -638), [-384, 386), [640, 1024)), possibly more clearly written as [-1024, -640], [-384, +384], [+640, +1024) or (with somewhat abusive notation) [-384, +384], [+640, -640].

For y = 0, the pixel is visible for [0, 480) and [1024, 1504) ([-1024, -544)). For y = 479, the pixel is visible for {0}, [546, 1026), and [1570, 2048) (also interpretable as {0}, [546, 1024), {1024}, and [1570, 2048) ({-1024} and [-478, 0)). These intervals overlap at {0} and {1024} ({-1024}) only, but nothing is drawn from [480, 546) and [1504, 1570) ([-544, -478)). The safe regions where exactly one occurrence is drawn are thus [0, 480), [546, 1024), [1024, 1504), and [1570, 2048) (or [-1024, -544) and [-478, 0)), possibly more clearly written as [-1024, -546], [-478, +478], [+546, +1024) or [-1024, -544), (-480, +480), (+544, +1024) or (with somewhat abusive notation) (-480, +480), (+544, -544).

In the above, [a, b) represents the half-open interval a <= x < b, while [a, b] represents the closed interval a <= x <= b and (a, b) represents the open interval a < x < b (yes, this notation is also used for points and thus is ambiguous; mathematicians somehow deal with this). To convert to the signed representation, I simply subtract 2048 (e.g. 1664 - 2048 = -384), and to convert a half-open interval to a closed interval, I subtract 2 from b (since the scissor offset works in groups of 2 by 2 pixels). [a, b] where a > b indicates instead a <= x < 1024 || -1024 <= x <= b within the implied range of [-1024, 1024).

Note that 1024 - 640 = 384 and 1024 - 480 = 544. Let w be the width or height. If w > 1024/2, then the safe region is [-1024, -w], [w - 1024, 1024 - w], and [w, 1024). If w < 1024/2, then the safe region is [-1024, w - 1024), (-w, w), (1024 - w, 1024), equivalent to [-1024, (w - 2) - 1024], [-(w - 2), w - 2], [1024 - (w - 2), 1024). In the case of w == 1024/2 == 512, then the first formula gives [-1024, -512], [-512, +512], [+512, +1024) while the second formula gives [-1024, -514], [-510, +510], [+514, +1024); the first is the whole interval [-1024, +1024) while the second is the whole interval excluding {-512} and {+512}. If the offset is 512 and the size is 512, nothing will be drawn, so the second formula should apply in the case that w == 1024/2.

Unfortunately there is a slight complication here: although the scissor rectangle is 640 by 480 in this test, the EFB itself is 640 by 528. Super Mario Galaxy, for instance, uses a 640 by 456 scissor rectangle and an offset of -464 (don't forget that offsets are subtracted, not added, to the coordinates - though I don't think it matters here). We still care about that rectangle because it can affect the EFB for y in the range [464, 528) - it might not show up on screen, but it does still affect rendering.

One other thing to note is that the pixel 0 0 is targetted for offsets of 0 0, 0 1024, and 1024 0 - so this isn't some kind of memory stride thing where an offset of 1024 hits the next row of pixels (producing an offset by 1). That would be harder to implement, and would also produce a 1-pixel jump in some cases.

Probably the simplest choice... is just checking the x0/x1/y0/y1 coordinates and seeing if they trigger a wrap, and forming rectangles for each one. That seems somewhat simple.

Something like this:

// TODO: This code assumes that bpmem.scissorBR is exclusive, i.e. the range is
// [bpmem.scissorTR.x, bpmem.scissorBR.x).  But it might be inclusive,
// in which case adding 1 or 2 to BR may be needed.
ASSERT(bpmem.scissorTL.x < bpmem.scissorBR.x);
ASSERT(bpmem.scissorTL.y < bpmem.scissorBR.y);
ASSERT(bpmem.scissorBR.x - bpmem.scissorTL.x < EFB_WIDTH);
ASSERT(bpmem.scissorBR.y - bpmem.scissorTL.y < EFB_HEIGHT);
int x0 = (bpmem.scissorTL.x - (bpmem.scissorOffset.x << 1)) & 1023;
int x1 = (bpmem.scissorBR.x - (bpmem.scissorOffset.x << 1)) & 1023;
int y0 = (bpmem.scissorTL.y - (bpmem.scissorOffset.y << 1)) & 1023;
int y1 = (bpmem.scissorBR.y - (bpmem.scissorOffset.y << 1)) & 1023;

// We previously had x0 < x1.  If x0 > x1 now, wrapping occurred.
std::vector<std::pair<int, int>> x_ranges;
x_ranges.reserve(2);
if (x0 > x1)
{
  // [0, x1) and [x0, 1024)
  x_ranges.emplace_back(0, x1);
  x_ranges.emplace_back(x0, 1024);
}
else if (x0 < x1)
{
  // [x0, x1)
  x_ranges.emplace_back(x0, x1);
}
else
{
  // x0 == x1; the range [x0, x0) is either nothing or everything
  // (would require hardware testing).
  // I don't think this can happen with the asserts requiring the
  // scissor rectangle's width to be less than the EFB width.
  ASSERT(false);
  // n.b. [x0, x0] IS something, specifically a 2-pixel wide strip.
  // This check is not excluding that.
}

std::vector<std::pair<int, int>> x_ranges_clamped;
x_ranges_clamped.reserve(x_ranges.size());
for (const auto& x_range : x_ranges) {
  // Intersect the range with the EFB.
  // Note that we don't need to compare with 0, as our mask with 1023 and then
  // the x0 < x1 rules mean that 0 < x0 < x1.
  // This step could be combined with the first step.
  if (x_range.first >= EFB_WIDTH)
  {
    // x0 too big - nothing visible at all.
    continue;
  }
  if (x_range.second > EFB_WIDTH)
  {
    // x1 too big - we can clamp it.  The first check ensures that clamping
    // will still have x0 < x1.
    x_ranges_clamped.emplace_back(x_range.first, EFB_WIDTH);
  }
  else
  {
    // The range is fine.
    x_ranges_clamped.push_back(range);
  }
}

// Do the exact same thing with Y (using EFB_HEIGHT instead of EFB_WIDTH)
std::vector<std::pair<int, int>> y_ranges;
// -snip-
std::vector<std::pair<int, int>> y_ranges_clamped;
// -snip-

// Now we need to form actual rectangles from the x and y ranges,
// which is a simple Cartesian product of x_ranges_clamped and y_ranges_clamped.
// Each rectangle is also a Cartesian product of x_range and y_range, with
// the rectangles being half-open (of the form [x0, x1) X [y0, y1)).
std::vector<MathUtil::Rectangle<int>> rectangles;
rectangles.reserve(x_ranges_clamped.size() * y_ranges_clamped.size());

for (const auto& x_range : x_ranges_clamped)
{
  for (const auto& y_range : y_ranges_clamped)
  {
    // Rectangle ctor takes x0, y0, x1, y1.
    rectangles.emplace_back(x_range.first, y_range.first, x_range.second, y_range.second);
  }
}

// Now, rectangles is a list of 0-4 scissor rectangles.  This algorithm should
// work, though it could obviously be simplified.

That algorithm only identifies the parts of the screen that will be affected by a draw, not what pixels are used for those. But it's a start. I still need to figure out how to have multiple scissor rectangles in practice - the OpenGL wiki articles Scissor Test and Vertex Post-Processing § Viewport array indicate that a geometry shader must be used for this, and the same thing probably applies to D3D.

Alright, as a first proof of concept, let's just use the largest rectangle. Also a few corrections to more accurately reflect how this would be implemented (though note that I haven't done any testing of this code yet; this is just thinking about properties of it).

// Range is [left, right] and [top, bottom] (closed intervals)
const int left = bpmem.scissorTL.x;
const int right = bpmem.scissorBR.x;
const int top = bpmem.scissorTL.y;
const int bottom = bpmem.scissorBR.y;
// These conditions are obvious
// What happens when these are violated hasn't yet been hardware tested
ASSERT(left <= right);
ASSERT(top <= bottom);
// Ensure the width/height are reasonable.  These also haven't been hardware tested.
// We want to be less than the width, not less or equal, as we use a closed interval.
ASSERT(right - left < EFB_WIDTH);
ASSERT(bottom - top < EFB_HEIGHT);
// Note that both the offsets and the coordinates have 342 added to them internally
// (for the offsets, this is before they are divided by 2/right shifted).
// This code could undo both sets of offsets, but it doesn't need to since they
// cancel out when subtracting.
const int xOff = (bpmem.scissorOffset.x << 1);
const int yOff = (bpmem.scissorOffset.y << 1);

// x0 and y0 live in the interval [0, 1023], while
// x1 and y1 live in the interval [1, 1024].  Thus, if no wrapping occurred (or both were wrapped),
// [x0, x1) and [y0, y1) would be valid half-open intervals.
// This also means that if left == right, x1 will always equal x0 + 1, without wrapping.
const int x0 = (left - xOff) & 1023;
const int x1 = ((right - xOff) & 1023) + 1;
const int y0 = (top - yOff) & 1023;
const int y1 = ((bottom - yOff) & 1023) + 1;

std::vector<std::pair<int, int>> x_ranges;
if (x0 < x1)
{
  // [x0, x1) is a valid interval, but it might not intersect with the EFB.
  if (x0 < EFB_WIDTH)
  {
    if (x1 <= EFB_WIDTH)
      x_ranges.emplace_back(x0, x1);
    else
      x_ranges.emplace_back(x0, EFB_WIDTH);
  }
}
else  // x0 >= x1, thus x1 <= x0
{
  // Wrapping occurred.  We need to make two intervals: [0, x1) and [x0, 1024).
  // However, we also only care about intervals that intersect the EFB.
  if (x1 <= EFB_WIDTH)
  {
    x_ranges.emplace_back(0, x1);
    // Since x1 <= x0, x0 < EFB_WIDTH only holds if x1 <= EFB_WIDTH
    if (x0 < EFB_WIDTH)
      x_ranges.emplace_back(x0, EFB_WIDTH);
  }

  // Note that for x0 == x1, [x0, x1) is not a valid half-open interval.
  // This would happen if, for instance, left = 1 and right = 0.  That would be
  // rejected by the assert that left < right, but we can still treat it as two intervals.
  // Further hardware testing is needed to determine if this is correct.
}

// Do the exact same thing with Y (using EFB_HEIGHT instead of EFB_WIDTH)
std::vector<std::pair<int, int>> y_ranges;
// -snip-

// Now we need to form actual rectangles from the x and y ranges,
// which is a simple Cartesian product of x_ranges and y_ranges.
// Each rectangle is also a Cartesian product of x_range and y_range, with
// the rectangles being half-open (of the form [x0, x1) X [y0, y1)).
std::vector<MathUtil::Rectangle<int>> rectangles;
rectangles.reserve(x_ranges.size() * y_ranges.size());

for (const auto& x_range : x_ranges)
{
  ASSERT(x_range.first < x_range.second);
  ASSERT(x_range.second <= EFB_WIDTH);
  for (const auto& y_range : y_ranges)
  {
    ASSERT(y_range.first < y_range.second);
    ASSERT(y_range.second <= EFB_WIDTH);

    // Rectangle ctor takes x0, y0, x1, y1.
    rectangles.emplace_back(x_range.first, y_range.first, x_range.second, y_range.second);
  }
}

// For now, simply choose the largest rectangle.
// But if we have no rectangles, add a bogus one that's out of bounds (this is temporary)
// Yes, this could be done more efficiently by looking at x_range and y_range individually,
// or even only picking one range earlier on, but again, this is temporary.
if (rectangles.empty())
  rectangles.emplace_back(1000, 1000, 1001, 1001);

MathUtil::Rectangle<int> rect =
    *std::max_element(rectangles.begin(), rectangles.end(), [](auto& a, auto& b) {
      return a.GetWidth() * a.GetHeight() < b.GetWidth() * b.GetHeight();
    });

This sorta works, but still needs adjustment to make a working viewport. Also, it still doesn't wrap right since the viewport itself doesn't have correct offsets to work with. Storing the original left in addition to the wrapped x0 would probably be enough (for [0, x0) that won't be correct though - I'll need to think about that further). One other problem is that the asserts at the top trigger accidentally, as BPFunctions::SetScissor gets called on any register change, so it gets called partway through the register change. A dirty system will probably be needed for it (like is already used in VertexShaderManager).

(Idea: But first, I can improve my visualization by scrolling the background for the offset - in that case, the x0/x1/y0/y1 values don't need to change. However multiple background positions might look weird... and definitely will result in confusion for SMG. OK, maybe don't do that just yet...)

After doing some further work, the result seems to work right in Dolphin. I still haven't gotten the multiple scissor rectangles/viewports to work (outside of the software renderer), but Major Minor renders correctly when just using the biggest one (except for a very short period during screen scrolls).

Unfortunately, the assumption that top <= bottom is violated in major_minor_title_long, because the scissor rectangle over the file name (or something like that) gets truncated when off screen, and top ends up being 822 (480) while bottom ends up being 821 (479). I'm pretty sure this isn't supposed to render anything, but it's something that needs to be hardware tested.

The other thing to note is that using a PAL video format (I used TVPal528IntDf; I'm not entirely sure what the "df" (doublefield?) part does, but my US TV gives really bad results with the non-df version while only giving moderately bad results with the df version. I can't capture component, only composite, so progressive isn't usable (and my Wii doesn't output progressive video correctly anymore either (due to failing capacitors?))). A video of the result can be seen here; I also added diagonal scrolling.

Using Dolphin's software renderer gives matching results, at least for the offsets where values change (the exact colors are sometimes off by 1); that's progress at least.

I also added some additional behaviors to the hardware test. Condensing it, here's what I get with PAL's 528 lines:

Pixel 0, 0:
x offset: 0 0 present, 640 0 absent, 1024 0 present, 1664 0 absent
y offset: 0 0 present, 0 528 absent, 0 1024 present, 0 1552 absent
diag offset: 0 0 present, 528 528 absent, 1024 1024 present, 1552 1552 absent
x pos: 0 0 present, 1 0 absent, 1706 0 present
y pos: 0 0 present, 1 0 absent, 1706 0 present
diag pos: 0 0 present, 1 1 absent, 1706 1706 present
x size (pos 0 0): 0 528 absent, 1 528 present, 1707 528 absent
x size 2 (pos 256 0): 0 528 absent, never present
y size (pos 0 0): 640 0 absent, 640 1 present, 640 1707 absent
y size 2 (pos 0 256): 640 0 absent, never present

Pixel 639 0:
x offset: 0 0 present, 2 0 absent, 386 0 present, 1026 0 absent, 1410 0 present
y offset: 0 0 present, 0 528 absent, 0 1024 present, 0 1552 absent
diag offset: 0 0 present, 2 2 absent, 386 386 present, 528 528 absent, 1024 1024 present, 1026 1026 absent, 1410 1410 present, 1552 1552 absent
x pos: 0 0 present, 640 0 absent
y pos: 0 0 present, 0 1 absent, 0 1706 present
diag pos: 0 0 present, 1 1 absent
x size (pos 0 0): 0 528 absent, 640 528 present, 1707 528 absent
x size 2 (pos 256 0): 0 528 absent, 384 528 present, 1451 528 absent
y size (pos 0 0): 640 0 absent, 640 1 present, 640 1707 absent
y size 2 (pos 0 256): 640 0 absent, never present

Pixel 0 527:
x offset: 0 0 present, 640 0 absent, 1024 0 present, 1664 0 absent
y offset: 0 0 present, 0 2 absent, 0 498 present, 0 1026 absent, 0 1522 present
diag offset: 0 0 present, 2 2 absent, 498 498 present, 640 640 absent, 1024 1024 present, 1026 1026 absent, 1522 1522 present, 1664 1664 absent
x pos: 0 0 present, 1 0 absent, 1706 0 present
y pos: 0 0 present, 0 528 absent
diag pos: 0 0 present, 1 1 absent
x size (pos 0 0): 0 528 absent, 1 528 present, 1707 528 absent
x size 2 (pos 256 0): 0 528 absent, never present
y size (pos 0 0): 640 0 absent, 640 528 present, 640 1707 absent
y size 2 (pos 0 256): 640 0 absent, 640 272 present, 640 1451 absent

Pixel 639 527:
x offset: 0 0 present, 2 0 absent, 386 0 present, 1026 0 absent, 1410 0 present
y offset: 0 0 present, 0 2 absent, 0 498 present, 0 1026 absent, 0 1522 present
diag offset: 0 0 present, 2 2 absent, 498 498 present, 1026 1026 absent, 1522 1522 present
x pos: 0 0 present, 640 0 absent
y pos: 0 0 present, 0 520 absent
diag pos: 0 0 present, 528 528 absent
x size (pos 0 0): 0 528 absent, 640 528 present, 1707 528 absent
x size 2 (pos 256 0): 0 528 absent, 384 528 present, 1451 528 absent
y size (pos 0 0): 640 0 absent, 640 528 present, 640 1707 absent
y size 2 (pos 0 256): 640 0 absent, 640 272 present, 640 1451 absent

It's worth noting that I typed this up from a video of running this (with some of the tests commented out so that the results fit on the screen); this is annoying and fiddly, so I'll add something to write it to a file.

The number 1706 is 2048 - 342. Also noteworthy is that the rectangle seems to pop in when x pos or y pos exceeds that; it's not a gradual transition (this makes the specific-pixel testing approach less useful). Note that this is when the values wrap around, i.e. when left < right, NOT factoring in the scissor offset necessarily, so it could be one of several things:

  • The scissor offset being set to 342 by default matters
  • The number 342 matters by itself (unlikely due to below)
  • left < right is checked directly and before the offset is applied, and since libogc and the SDK add 342 automatically, that check fails if it wraps.

I think the last is the most likely. But it's awkward to think about when using libogc; I should write to the BP registers directly to make it clearer. That would also eliminate the confusion about width, since the actual registers are left and right.


Just to have it out for reference, this is the code I currently have in Dolphin. ComputeScissorRects is used by the software renderer (currently without caching), while ComputeScissorRect is used by the hardware renderers. This code handles offsets correctly, but doesn't seem to handle the rest right.

static std::vector<ScissorRect::ScissorRange> ComputeScissorRanges(int start, int end, int offset,
                                                                   int efb_dim)
{
  // [start, end] is a closed interval.  We want to make a half-open interval.
  // p0 lives in the interval [0, 1023], while p1 lives in the interval [1, 1024] since we add 1.
  // Thus, if no wrapping occurred (or both were wrapped into the same region), [p0, p1) would be a
  // valid half-open interval. This also means that if start == end, p1 will always equal p0 + 1,
  // regardless of wrapping.
  const int p0 = (start - offset) & 1023;
  const int p1 = ((end - offset) & 1023) + 1;

  std::vector<ScissorRect::ScissorRange> ranges;

  if (p0 < p1)
  {
    // [p0, p1) is a valid interval, but it might not intersect with the EFB.
    if (p0 < efb_dim)
    {
      // p0 = start - offset, so without wrapping offset = start - p0.  This same equation gives the
      // new offset with wrapping applied.
      if (p1 <= efb_dim)
        ranges.emplace_back(start - p0, p0, p1);
      else
        ranges.emplace_back(start - p0, p0, efb_dim);
    }
  }
  else  // p0 >= p1, thus p1 <= p0
  {
    // Wrapping occurred.  We need to make two intervals: [0, p1) and [p0, 1024).
    // However, we also only care about intervals that intersect the EFB.
    if (p1 <= efb_dim)
    {
      // The offset here is anchored on p1 and end, but since end is inclusive and p1 is exclusive
      // we need to subtract 1.
      ranges.emplace_back(end - p1 - 1, 0, p1);
      // Since p1 <= p0, p0 < efb_dim only holds if p1 <= efb_dim
      if (p0 < efb_dim)
        ranges.emplace_back(start - p0, p0, efb_dim);
    }

    // Note that for p0 == p1, [p0, p1) is not a valid half-open interval.
    // This would happen if, for instance, start = 1 and end = 0.  That would be
    // rejected by the assert that start <= end, but we can still treat it as two intervals.
    // Further hardware testing is needed to determine if this is correct.
  }

  return ranges;
}

std::vector<ScissorRect> ComputeScissorRects()
{
  // Range is [left, right] and [top, bottom] (closed intervals)
  const int left = bpmem.scissorTL.x;
  const int right = bpmem.scissorBR.x;
  const int top = bpmem.scissorTL.y;
  const int bottom = bpmem.scissorBR.y;
  // These conditions are obvious
  // What happens when these are violated hasn't yet been hardware tested
  ASSERT(left <= right);
  ASSERT(top <= bottom);
  // Ensure the width/height are reasonable.  These also haven't been hardware tested.
  // We want to be less than the width, not less or equal, as we use a closed interval.
  ASSERT(right - left < EFB_WIDTH);
  ASSERT(bottom - top < EFB_HEIGHT);
  // Note that both the offsets and the coordinates have 342 added to them internally
  // (for the offsets, this is before they are divided by 2/right shifted).
  // This code could undo both sets of offsets, but it doesn't need to since they
  // cancel out when subtracting.
  const int x_off = (bpmem.scissorOffset.x << 1);
  const int y_off = (bpmem.scissorOffset.y << 1);

  std::vector<ScissorRect::ScissorRange> x_ranges =
      ComputeScissorRanges(left, right, x_off, EFB_WIDTH);
  std::vector<ScissorRect::ScissorRange> y_ranges =
      ComputeScissorRanges(top, bottom, y_off, EFB_HEIGHT);

  // Now we need to form actual rectangles from the x and y ranges,
  // which is a simple Cartesian product of x_ranges_clamped and y_ranges_clamped.
  // Each rectangle is also a Cartesian product of x_range and y_range, with
  // the rectangles being half-open (of the form [x0, x1) X [y0, y1)).
  std::vector<ScissorRect> rectangles;
  rectangles.reserve(std::min(x_ranges.size() * y_ranges.size(), 1ULL));

  for (const auto& x_range : x_ranges)
  {
    DEBUG_ASSERT(x_range.start < x_range.end);
    DEBUG_ASSERT(x_range.end <= EFB_WIDTH);
    for (const auto& y_range : y_ranges)
    {
      DEBUG_ASSERT(y_range.start < y_range.end);
      DEBUG_ASSERT(y_range.end <= EFB_HEIGHT);
      rectangles.emplace_back(x_range, y_range);
    }
  }

  return rectangles;
}

ScissorRect ComputeScissorRect()
{
  std::vector<ScissorRect> rectangles = ComputeScissorRects();

  // For now, simply choose the largest rectangle.
  // But if we have no rectangles, add a bogus one that's out of bounds (this is temporary)
  // Yes, this could be done more efficiently by looking at x_range and y_range individually,
  // or even only picking one range earlier on, but again, this is temporary.
  if (rectangles.empty())
  {
    rectangles.emplace_back(ScissorRect::ScissorRange{0, 1000, 1001},
                            ScissorRect::ScissorRange{0, 1000, 1001});
  }

  return *std::max_element(rectangles.begin(), rectangles.end());
}

Looking at libogc, [GX_SetScissor](https://github.com/devkitPro/libogc/blob/bd24a9b3f59502f9b30d6bac0ae35fc485045f78/libogc/gx.c#L3941-L3945_ uses an 11-bit mask (0x7ff) but shifts by 12. Except it uses a 12-bit mask (0xfff) for y1 (which it calls nht). In Major Minor at least, GXSetScissor uses 11-bit masks in all cases, and in fact uses the original value masked with 0xff800800, making it clear that it doesn't use those middle bits. GX_SetScissorBoxOffset and GXSetScissorBoxOffset both use 10-bit masks (0x3ff) and shifts, but it seems like the 10th bit is actually unused. I'm guessing the 12th bit is also unused, but that's something I'll need to test too... meaning x0/x1 can range from 0 to 4096. But given that y1 uses the 12-bit mask on libogc and I got the wrapping behavior with it in my current test, I suspect only 11 bits are used.


A few days later, and I now understand the difference between TVPal528Int and TVPal528IntDf: the first uses only one line in the copy filter, while the second blurs all 3 lines. That's no good; it breaks the test if I try to read the EFB back. I think the artefacts I saw on my TV were it picking the wrong field as the first interlaced field, so the odd and even lines were swapped - since it's not designed to support PAL signals, I guess it makes sense that that happens.

Another thing I've found is that very strange stuff happens if the GX registers are written directly: instead of

GX_SetScissor(x0, y0, Width(), Height());
GX_SetScissorBoxOffset(xOff, yOff);

using this:

// GX_SetScissor and GX_SetScissorBoxOffset internally add an offset.
// We want the raw registers instead.
// Also, these fields are normally treated as 11 bits (though libogc uses 12 for y1);
// however, to test for wrapping let's use the full 12 bits.
u32 tl = 0x20000000 | ((x0 & 0xfff) << 12) | (y0 & 0xfff);
u32 br = 0x21000000 | ((x1 & 0xfff) << 12) | (y1 & 0xfff);
// This field is treated as 10 bits, but testing seems to indicate that it's 9 bits.
u32 off = 0x59000000 | (((yOff >> 1) & 0x3ff) << 10) | ((xOff >> 1) & 0x3ff);
LoadPBReg(tl);
LoadPBReg(br);
LoadPBReg(off)

causes things to be visually offset. I'm guessing this is related to the 342-offset applied to the viewport as well as the x-offset normally. What's odd is that if I add a second quad ranging from -1 to +2, that quad can wrap around and intersect with itself. Which is really odd.

Dolphin never shows the area outside the viewport, not even with the software renderer. That's a problem. Does this affect actual games and not just my jank test case? I'm not sure.

I also think that the top bit doesn't matter for x0/y0/x1/y1; they range from 0-2047 and not 0-4095. But I'm not 100% sure of this; my test for it is a bit jank.

A new cursed result: https://i.imgur.com/gVohSm5.png (https://i.imgur.com/Cx2MolM.png) - libogc mentions bad stuff happening with clipping when using negative origin values in the viewport. This is probably the same deal.


After a bit more experimentation, and after not drawing anything when left > right or top > bottom, things are pretty close (when considering the stuff in the viewport only, and not stuff outside of the viewport that may or may not be clipped, and also when restricting to cases where at most one rectangle should be visible).

There's at least one more issue, though. When testing changing x0/left, this is the result on hardware:

x0 1704 y0    0 x1  639 y1  527 xOff    0 yOff    0: 0 rect(s)
x0 1705 y0    0 x1  639 y1  527 xOff    0 yOff    0: 0 rect(s)
x0 1706 y0    0 x1  639 y1  527 xOff    0 yOff    0: 1 rect(s)
[0, 639] X [0, 527]: 000001, 00fd01, fe0001, fdfd01
x0 1707 y0    0 x1  639 y1  527 xOff    0 yOff    0: 1 rect(s)
[0, 639] X [0, 527]: 000001, 00fd01, fe0001, fdfd01
x0 1708 y0    0 x1  639 y1  527 xOff    0 yOff    0: 1 rect(s)
[0, 639] X [0, 527]: 000001, 00fd01, fe0001, fdfd01

but this is what Dolphin gives:

x0 1704 y0    0 x1  639 y1  527 xOff    0 yOff    0: 0 rect(s)
x0 1705 y0    0 x1  639 y1  527 xOff    0 yOff    0: 0 rect(s)
x0 1706 y0    0 x1  639 y1  527 xOff    0 yOff    0: 1 rect(s)
[2, 639] X [0, 527]: 000001, 00fe01, fe0001, fefe01
x0 1707 y0    0 x1  639 y1  527 xOff    0 yOff    0: 1 rect(s)
[2, 639] X [0, 527]: 000001, 00fe01, fe0001, fefe01
x0 1708 y0    0 x1  639 y1  527 xOff    0 yOff    0: 1 rect(s)
[2, 639] X [0, 527]: 000001, 00fe01, fe0001, fefe01

Ignoring the difference in color, this is still an incorrect size for the rectangles for some reason. It's worth noting that 1706 is 2048 - 342, so this is probably a case where things are wrapping around as this is where the GX registers wrap around (since they add 342 and then mask with 2047 (possibly independent of the actual size of the field in hardware, but I think it is masked with 2047 and not 4095 in hardware too)). In other words, instead of the horizontal range being [1706, 639], it's [-342, 639].

This ends up calling ComputeScissorRanges(start = 0, end = 981, offset = 342, efb_dim = 640); this gives p0 = -342 & 1023 = 682 and p1 = 640. p1 >= p0, so it thinks wrapping occurred; furthermore, p1 <= efb_dim (since they're both 640), though p1 >= efb_dim. This means that the only generated scissor rectangle is offset = end - p1 - 1 = 981 - 640 - 1 = 340, start=0, end=p1=640. The offset should instead be 342, though the rest of the values are fine.

#---------------------------------------------------------------------------------
# Clear the implicit built in rules
#---------------------------------------------------------------------------------
.SUFFIXES:
.SECONDARY:
#---------------------------------------------------------------------------------
ifeq ($(strip $(DEVKITPPC)),)
$(error "Please set DEVKITPPC in your environment. export DEVKITPPC=<path to>devkitPPC")
endif
include $(DEVKITPPC)/wii_rules
#---------------------------------------------------------------------------------
# TARGET is the name of the output
# BUILD is the directory where object files & intermediate files will be placed
# SOURCES is a list of directories containing source code
# INCLUDES is a list of directories containing extra header files
#---------------------------------------------------------------------------------
TARGET := $(notdir $(CURDIR))
BUILD := build
SOURCES := .
DATA := .
TEXTURES := .
INCLUDES :=
#---------------------------------------------------------------------------------
# options for code generation
#---------------------------------------------------------------------------------
CFLAGS = -g -O2 -Wall $(MACHDEP) $(INCLUDE)
CXXFLAGS = $(CFLAGS)
LDFLAGS = -g $(MACHDEP) -Wl,-Map,$(notdir $@).map
#---------------------------------------------------------------------------------
# any extra libraries we wish to link with the project
#---------------------------------------------------------------------------------
LIBS := -lwiiuse -lbte -logc -lm
#---------------------------------------------------------------------------------
# list of directories containing libraries, this must be the top level containing
# include and lib
#---------------------------------------------------------------------------------
LIBDIRS := $(PORTLIBS)
#---------------------------------------------------------------------------------
# no real need to edit anything past this point unless you need to add additional
# rules for different file extensions
#---------------------------------------------------------------------------------
ifneq ($(BUILD),$(notdir $(CURDIR)))
#---------------------------------------------------------------------------------
export OUTPUT := $(CURDIR)/$(TARGET)
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
$(foreach dir,$(DATA),$(CURDIR)/$(dir)) \
$(foreach dir,$(TEXTURES),$(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
#---------------------------------------------------------------------------------
# automatically build a list of object files for our project
#---------------------------------------------------------------------------------
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
sFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.S)))
BINFILES :=
SCFFILES := $(foreach dir,$(TEXTURES),$(notdir $(wildcard $(dir)/*.scf)))
TPLFILES := $(SCFFILES:.scf=.tpl)
#---------------------------------------------------------------------------------
# use CXX for linking C++ projects, CC for standard C
#---------------------------------------------------------------------------------
ifeq ($(strip $(CPPFILES)),)
export LD := $(CC)
else
export LD := $(CXX)
endif
export OFILES_BIN := $(addsuffix .o,$(BINFILES)) $(addsuffix .o,$(TPLFILES))
export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(sFILES:.s=.o) $(SFILES:.S=.o)
export OFILES := $(OFILES_BIN) $(OFILES_SOURCES)
export HFILES := $(addsuffix .h,$(subst .,_,$(BINFILES))) $(addsuffix .h,$(subst .,_,$(TPLFILES)))
#---------------------------------------------------------------------------------
# build a list of include paths
#---------------------------------------------------------------------------------
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD) \
-I$(LIBOGC_INC)
#---------------------------------------------------------------------------------
# build a list of library paths
#---------------------------------------------------------------------------------
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) \
-L$(LIBOGC_LIB)
export OUTPUT := $(CURDIR)/$(TARGET)
.PHONY: $(BUILD) clean
#---------------------------------------------------------------------------------
$(BUILD):
@[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
#---------------------------------------------------------------------------------
clean:
@echo clean ...
@rm -fr $(BUILD) $(OUTPUT).elf $(OUTPUT).dol
#---------------------------------------------------------------------------------
run:
wiiload $(OUTPUT).dol
#---------------------------------------------------------------------------------
else
#---------------------------------------------------------------------------------
# main targets
#---------------------------------------------------------------------------------
$(OUTPUT).dol: $(OUTPUT).elf
$(OUTPUT).elf: $(OFILES)
$(OFILES_SOURCES) : $(HFILES)
#---------------------------------------------------------------------------------
%.txt.o %_txt.h : %.txt
#---------------------------------------------------------------------------------
@echo $(notdir $<)
@$(bin2o)
#---------------------------------------------------------------------------------
%.tpl.o %_tpl.h : %.tpl
#---------------------------------------------------------------------------------
@echo $(notdir $<)
@$(bin2o)
-include $(DEPSDIR)/*.d
#---------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------
View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment