shithub: zelda3

Download patch

ref: 80de835a081d9312b5cc2f557037f82d84cc7c9b
parent: 1f8985903a326287ee4c5098316a6caa3bdd9467
author: Snesrev <snesrev@protonmail.com>
date: Wed Sep 7 00:26:36 EDT 2022

Higher Quality Mode7. Fixes #38

 - Draws mode7 with 4x the width/height.
 - Can be turned off in ini file.

--- a/config.c
+++ b/config.c
@@ -12,6 +12,8 @@
   kKeyMod_Ctrl = 0x1000,
 };
 
+Config g_config;
+
 #define REMAP_SDL_KEYCODE(key) ((key) & SDLK_SCANCODE_MASK ? kKeyMod_ScanCode : 0) | (key) & (kKeyMod_ScanCode - 1)
 #define _(x) REMAP_SDL_KEYCODE(x)
 #define S(x) REMAP_SDL_KEYCODE(x) | kKeyMod_Shift
@@ -186,6 +188,8 @@
 static int GetIniSection(const char *s) {
   if (StringEqualsNoCase(s, "[KeyMap]"))
     return 0;
+  if (StringEqualsNoCase(s, "[Graphics]"))
+    return 1;
   return -1;
 }
 
@@ -198,6 +202,15 @@
         return true;
       }
     }
+  } else if (section == 1) {
+    if (StringEqualsNoCase(key, "EnhancedMode7")) {
+      g_config.enhanced_mode7 = atoi(value);
+      return true;
+    } else if (StringEqualsNoCase(key, "NewRenderer")) {
+      g_config.new_renderer = atoi(value);
+      return true;
+    }
+
   }
   return false;
 }
--- a/config.h
+++ b/config.h
@@ -31,6 +31,13 @@
   kKeys_Total,
 };
 
+typedef struct Config {
+  bool enhanced_mode7;
+  bool new_renderer;
+} Config;
+
+extern Config g_config;
+
 uint8 *ReadFile(const char *name, size_t *length);
 void ParseConfigFile();
 void AfterConfigParse();
--- a/main.c
+++ b/main.c
@@ -28,6 +28,7 @@
 static int g_input1_state;
 static bool g_display_perf;
 static int g_curr_fps;
+static int g_ppu_render_flags = 0;
 
 void PatchRom(uint8 *rom);
 void SetSnes(Snes *snes);
@@ -124,12 +125,13 @@
   kSnesSamplesPerBlock = (534 * kSampleRate) / 32000,
 };
 
-static void RenderScreenWithPerf(uint32 *pixel_buffer) {
+static bool RenderScreenWithPerf(uint8 *pixel_buffer, size_t pitch, uint32 render_flags) {
+  bool rv;
   if (g_display_perf) {
     static float history[64], average;
     static int history_pos;
     uint64 before = SDL_GetPerformanceCounter();
-    ZeldaDrawPpuFrame(pixel_buffer);
+    rv = ZeldaDrawPpuFrame(pixel_buffer, pitch, render_flags);
     uint64 after = SDL_GetPerformanceCounter();
     float v = (double)SDL_GetPerformanceFrequency() / (after - before);
     average += v - history[history_pos];
@@ -137,8 +139,9 @@
     history_pos = (history_pos + 1) & 63;
     g_curr_fps = average * (1.0f / 64);
   } else {
-    ZeldaDrawPpuFrame(pixel_buffer);
+    rv = ZeldaDrawPpuFrame(pixel_buffer, pitch, render_flags);
   }
+  return rv;
 }
 
 
@@ -147,6 +150,8 @@
   ParseConfigFile();
   AfterConfigParse();
 
+  g_ppu_render_flags = g_config.new_renderer * kPpuRenderFlags_NewRenderer | g_config.enhanced_mode7 * kPpuRenderFlags_4x4Mode7;
+
   // set up SDL
   if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) != 0) {
     printf("Failed to init SDL: %s\n", SDL_GetError());
@@ -166,7 +171,7 @@
   }
   g_renderer = renderer;
   SDL_RenderSetLogicalSize(renderer, 512, 480); 
-  SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBX8888, SDL_TEXTUREACCESS_STREAMING, kRenderWidth, kRenderHeight);
+  SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBX8888, SDL_TEXTUREACCESS_STREAMING, kRenderWidth * 2, kRenderHeight * 2);
   if(texture == NULL) {
     printf("Failed to create texture: %s\n", SDL_GetError());
     return 1;
@@ -331,7 +336,7 @@
   }
 }
 
-static void RenderDigit(uint32 *dst, int digit, uint32 color) {
+static void RenderDigit(uint8 *dst, size_t pitch, int digit, uint32 color, bool big) {
   static const uint8 kFont[] = {
     0x1c, 0x36, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x36, 0x1c,
     0x18, 0x1c, 0x1e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e,
@@ -345,38 +350,52 @@
     0x3e, 0x63, 0x63, 0x63, 0x7e, 0x60, 0x60, 0x60, 0x30, 0x1e,
   };
   const uint8 *p = kFont + digit * 10;
-  for (int y = 0; y < 10; y++, dst += 512) {
-    int v = *p++;
-    for (int x = 0; v; x++, v>>=1) {
-      if (v & 1)
-        dst[x] = color;
+  if (!big) {
+    for (int y = 0; y < 10; y++, dst += pitch) {
+      int v = *p++;
+      for (int x = 0; v; x++, v >>= 1) {
+        if (v & 1)
+          ((uint32 *)dst)[x] = color;
+      }
     }
+  } else {
+    for (int y = 0; y < 10; y++, dst += pitch * 2) {
+      int v = *p++;
+      for (int x = 0; v; x++, v >>= 1) {
+        if (v & 1) {
+          ((uint32 *)dst)[x * 2 + 1] = ((uint32 *)dst)[x * 2] = color;
+          ((uint32 *)(dst+pitch))[x * 2 + 1] = ((uint32 *)(dst + pitch))[x * 2] = color;
+        }
+      }
+    }
   }
 }
 
-static void RenderNumber(uint32 *dst, int n) {
+static void RenderNumber(uint8 *dst, size_t pitch, int n, bool big) {
   char buf[32], *s;
   int i;
   sprintf(buf, "%d", n);
-  for (s = buf, i = 2; *s; s++, i += 8)
-    RenderDigit(dst + 513 + i, *s - '0', 0x40404000);
-  for (s = buf, i = 2; *s; s++, i += 8)
-    RenderDigit(dst + i, *s - '0', 0xffffff00);
+  for (s = buf, i = 2 * 4; *s; s++, i += 8 * 4)
+    RenderDigit(dst + ((pitch + i + 4) << big), pitch, *s - '0', 0x40404000, big);
+  for (s = buf, i = 2 * 4; *s; s++, i += 8 * 4)
+    RenderDigit(dst + (i << big), pitch, *s - '0', 0xffffff00, big);
 }
 
 static void RenderScreen(SDL_Window *window, SDL_Renderer *renderer, SDL_Texture *texture, bool fullscreen) {
-  void* pixels = NULL;
+  uint8* pixels = NULL;
   int pitch = 0;
-  if(SDL_LockTexture(texture, NULL, &pixels, &pitch) != 0) {
+  if(SDL_LockTexture(texture, NULL, (void**)&pixels, &pitch) != 0) {
     printf("Failed to lock texture: %s\n", SDL_GetError());
     return;
   }
-  RenderScreenWithPerf((uint32 *)pixels);
+  bool hq = RenderScreenWithPerf(pixels, pitch, g_ppu_render_flags);
   if (g_display_perf)
-    RenderNumber((uint32 *)pixels + 512*2, g_curr_fps);
+    RenderNumber(pixels + (pitch*2<<hq), pitch, g_curr_fps, hq);
   SDL_UnlockTexture(texture);
   SDL_RenderClear(renderer);
-  SDL_RenderCopy(renderer, texture, NULL, NULL);
+
+  SDL_Rect src_rect = { 0, 0, 512, 480 };
+  SDL_RenderCopy(renderer, texture, hq ? NULL : &src_rect, NULL);
 }
 
 static void HandleCommand(uint32 j, bool pressed) {
@@ -426,7 +445,7 @@
     case kKeys_ZoomIn: DoZoom(1); break;
     case kKeys_ZoomOut: DoZoom(-1); break;
     case kKeys_DisplayPerf: g_display_perf ^= 1; break;
-    case kKeys_ToggleRenderer: g_zenv.ppu->newRenderer ^= 1; break;
+    case kKeys_ToggleRenderer: g_ppu_render_flags ^= kPpuRenderFlags_NewRenderer; break;
     default: assert(0);
     }
   }
--- a/snes/ppu.c
+++ b/snes/ppu.c
@@ -27,7 +27,6 @@
 Ppu* ppu_init(Snes* snes) {
   Ppu* ppu = (Ppu * )malloc(sizeof(Ppu));
   ppu->snes = snes;
-  ppu->newRenderer = true;
   return ppu;
 }
 
@@ -137,12 +136,42 @@
   func(ctx, &ppu->vram, offsetof(Ppu, mosaicModulo) - offsetof(Ppu, vram));
 }
 
-void PpuBeginDrawing(Ppu *ppu, uint32_t *pixels) {
-  ppu->renderBuffer = pixels + 512 * 16;
+bool PpuBeginDrawing(Ppu *ppu, uint8_t *pixels, size_t pitch, uint32_t render_flags) {
+  ppu->renderFlags = render_flags;
+  bool hq = ppu->mode == 7 && !ppu->forcedBlank && 
+      (ppu->renderFlags & (kPpuRenderFlags_4x4Mode7 | kPpuRenderFlags_NewRenderer)) == (kPpuRenderFlags_4x4Mode7 | kPpuRenderFlags_NewRenderer);
+  ppu->renderPitch = (uint)pitch;
+  ppu->renderBuffer = pixels + (pitch * 16 << hq);
 
-  // clear top 16 and last 16 lines
-  memset(pixels, 0, 512 * 16 * 4);
-  memset(pixels + (464 * 512), 0, 512 * 16 * 4);
+  // clear top 16 and last 16 lines (or 32 in hq)
+  int w = 512 * 4 << hq;
+  int n = 16 << hq;
+  int y1 = 464 << hq;
+  for (size_t i = 0; i < n; i++) {
+    memset(pixels + pitch * i, 0, w);
+    memset(pixels + pitch * (y1 + i), 0, w);
+  }
+
+  // Cache the brightness computation
+  if (ppu->brightness != ppu->lastBrightnessMult) {
+    uint8_t ppu_brightness = ppu->brightness;
+    ppu->lastBrightnessMult = ppu_brightness;
+    for (int i = 0; i < 32; i++)
+      ppu->brightnessMultHalf[i * 2] = ppu->brightnessMultHalf[i * 2 + 1] = ppu->brightnessMult[i] =
+      ((i << 3) | (i >> 2)) * ppu_brightness / 15;
+    // Store 31 extra entries to remove the need for clamping to 31.
+    memset(&ppu->brightnessMult[32], ppu->brightnessMult[31], 31);
+  }
+
+  if (hq) {
+    for (int i = 0; i < 256; i++) {
+      uint32 color = ppu->cgram[i];
+      ppu->colorMapRgb[i] = ppu->brightnessMult[color & 0x1f] << 24 | ppu->brightnessMult[(color >> 5) & 0x1f] << 16 | ppu->brightnessMult[(color >> 10) & 0x1f] << 8;
+    }
+  }
+
+  
+  return hq;
 }
 
 void ppu_runLine(Ppu *ppu, int line) {
@@ -165,19 +194,17 @@
     ppu->lineHasSprites = !ppu->forcedBlank && ppu_evaluateSprites(ppu, line - 1);
 
     // actual line
-    if (ppu->newRenderer) {
+    if (ppu->renderFlags & kPpuRenderFlags_NewRenderer) {
       PpuDrawWholeLine(ppu, line);
     } else {
       if (ppu->mode == 7)
         ppu_calculateMode7Starts(ppu, line);
-      for (int x = 0; x < 256; x++) {
+      for (int x = 0; x < 256; x++)
         ppu_handlePixel(ppu, x, line);
-      }
+      uint8 *dst = ppu->renderBuffer + ((line - 1) * 2 * ppu->renderPitch);
+      memcpy(dst + ppu->renderPitch, dst, 512 * 4);
     }
 
-    // Duplicate each line
-    uint32 *dst = &ppu->renderBuffer[(line - 1) * 1024];
-    memcpy(dst + 512, dst, 512 * 4);
   }
 }
 
@@ -672,6 +699,86 @@
   }
 }
 
+uint16 g_mode7_lo, g_mode7_hi;
+void PpuSetMode7PerspectiveCorrection(Ppu *ppu, int low, int high) {
+  ppu->mode7PerspectiveLow = low ? 1.0f / low : 0.0f;
+  ppu->mode7PerspectiveHigh = 1.0f / high;
+}
+
+static FORCEINLINE float FloatInterpolate(float x, float xmin, float xmax, float ymin, float ymax) {
+  return ymin + (ymax - ymin) * (x - xmin) * (1.0f / (xmax - xmin));
+}
+
+// Upsampled version of mode7 rendering. Draws everything in 4x the normal resolution.
+// Draws directly to the pixel buffer and bypasses any math, and supports only
+// a subset of the normal features (all that zelda needs)
+static void PpuDrawMode7Upsampled(Ppu *ppu, uint y) {
+  // expand 13-bit values to signed values
+  uint32 xCenter = ((int16_t)(ppu->m7matrix[4] << 3)) >> 3, yCenter = ((int16_t)(ppu->m7matrix[5] << 3)) >> 3;
+  uint32 clippedH = (((int16_t)(ppu->m7matrix[6] << 3)) >> 3) - xCenter;
+  uint32 clippedV = (((int16_t)(ppu->m7matrix[7] << 3)) >> 3) - yCenter;
+  uint32 m0 = ppu->m7matrix[0];  // xpos increment per horiz movement
+  uint32 m3 = ppu->m7matrix[3];  // ypos increment per vert movement
+  uint8 *dst_start = &ppu->renderBuffer[(y - 1) * 4 * ppu->renderPitch], *dst_end, *dst = dst_start;
+  int32 m0v[4];
+  if (*(uint32*)&ppu->mode7PerspectiveLow == 0) {
+    m0v[0] = m0v[1] = m0v[2] = m0v[3] = ppu->m7matrix[0] << 12;
+  } else {
+    static const float kInterpolateOffsets[4] = { -1, -1 + 0.25f, -1 + 0.5f, -1 + 0.75f };
+    for (int i = 0; i < 4; i++)
+      m0v[i] = 4096.0f / FloatInterpolate((int)y + kInterpolateOffsets[i], 0, 223, ppu->mode7PerspectiveLow, ppu->mode7PerspectiveHigh);
+  }
+
+  for (int j = 0; j < 4; j++) {
+    m0 = m3 = m0v[j];
+    uint32 m1 = ppu->m7matrix[1] << 12;  // xpos increment per vert movement
+    uint32 m2 = ppu->m7matrix[2] << 12;  // ypos increment per horiz movement
+    uint32 xpos = m0 * clippedH + m1 * (clippedV + y) + (xCenter << 20), xcur;
+    uint32 ypos = m2 * clippedH + m3 * (clippedV + y) + (yCenter << 20), ycur;
+
+    uint32 tile, pixel;
+    xpos -= (m0 + m1) >> 1;
+    ypos -= (m2 + m3) >> 1;
+    xcur = (xpos << 2) + j * m1;
+    ycur = (ypos << 2) + j * m3;
+    dst_end = dst + 4096;
+
+#define DRAW_PIXEL() \
+    tile = ppu->vram[(ycur >> 25 & 0x7f) * 128 + (xcur >> 25 & 0x7f)] & 0xff;  \
+    pixel = ppu->vram[tile * 64 + (ycur >> 22 & 7) * 8 + (xcur >> 22 & 7)] >> 8; \
+    *(uint32*)dst = ppu->colorMapRgb[pixel]; \
+    xcur += m0, ycur += m2, dst += 4;
+
+    do {
+      DRAW_PIXEL();
+      DRAW_PIXEL();
+      DRAW_PIXEL();
+      DRAW_PIXEL();
+    } while (dst != dst_end);
+#undef DRAW_PIXEL
+    dst += (ppu->renderPitch - 4096);
+  }
+
+  if (ppu->lineHasSprites) {
+    uint8 *pixels = ppu->objBuffer.pixel;
+    size_t pitch = ppu->renderPitch;
+    for (size_t i = 0; i < 256; i++) {
+      uint32 pixel = pixels[i];
+      if (pixel) {
+        uint32 color = ppu->colorMapRgb[pixel];
+        uint8 *dst = dst_start + i * 16;
+
+        ((uint32 *)dst)[3] = ((uint32 *)dst)[2] = ((uint32 *)dst)[1] = ((uint32 *)dst)[0] = color;
+        ((uint32 *)(dst + pitch * 1))[3] = ((uint32 *)(dst + pitch * 1))[2] = ((uint32 *)(dst + pitch * 1))[1] = ((uint32 *)(dst + pitch * 1))[0] = color;
+        ((uint32 *)(dst + pitch * 2))[3] = ((uint32 *)(dst + pitch * 2))[2] = ((uint32 *)(dst + pitch * 2))[1] = ((uint32 *)(dst + pitch * 2))[0] = color;
+        ((uint32 *)(dst + pitch * 3))[3] = ((uint32 *)(dst + pitch * 3))[2] = ((uint32 *)(dst + pitch * 3))[1] = ((uint32 *)(dst + pitch * 3))[0] = color;
+      }
+    }
+  }
+
+#undef DRAW_PIXEL
+}
+
 static void PpuDrawBackgrounds(Ppu *ppu, int y, bool sub) {
 // Top 4 bits contain the prio level, and bottom 4 bits the layer type.
 // SPRITE_PRIO_TO_PRIO can be used to convert from obj prio to this prio.
@@ -716,24 +823,21 @@
 
 static NOINLINE void PpuDrawWholeLine(Ppu *ppu, uint y) {
   if (ppu->forcedBlank) {
-    uint32 *dst = &ppu->renderBuffer[(y - 1) * 1024];
-    for (int i = 0; i < 256; i++, dst += 2) {
-      dst[1] = dst[0] = 0;
+    uint8 *dst = &ppu->renderBuffer[(y - 1) * 2 * ppu->renderPitch];
+    size_t pitch = ppu->renderPitch;
+    for (int i = 0; i < 256; i++, dst += 8) {
+      ((uint32*)dst)[1] = ((uint32 *)dst)[0] = 0;
+      ((uint32*)(dst + pitch))[1] = ((uint32*)(dst + pitch))[0] = 0;
     }
     return;
   }
 
-  // Cache the brightness computation
-  if (ppu->brightness != ppu->lastBrightnessMult) {
-    uint8_t ppu_brightness = ppu->brightness;
-    ppu->lastBrightnessMult = ppu_brightness;
-    for (int i = 0; i < 32; i++)
-      ppu->brightnessMultHalf[i * 2] = ppu->brightnessMultHalf[i * 2 + 1] = ppu->brightnessMult[i] =
-          ((i << 3) | (i >> 2)) * ppu_brightness / 15;
-    // Store 31 extra entries to remove the need for clamping to 31.
-    memset(&ppu->brightnessMult[32], ppu->brightnessMult[31], 31);
+  if (ppu->mode == 7 && (ppu->renderFlags & kPpuRenderFlags_4x4Mode7)) {
+    PpuDrawMode7Upsampled(ppu, y);
+    return;
   }
 
+
   // Default background is backdrop
   memset(&ppu->bgBuffers[0].pixel, 0, sizeof(ppu->bgBuffers[0].pixel));
   memset(&ppu->bgBuffers[0].prio, 0x05, sizeof(ppu->bgBuffers[0].prio));
@@ -767,7 +871,7 @@
   uint32 cw_clip_math = ((cwin.bits & kCwBitsMod[ppu->clipMode]) ^ kCwBitsMod[ppu->clipMode + 4]) |
                         ((cwin.bits & kCwBitsMod[ppu->preventMathMode]) ^ kCwBitsMod[ppu->preventMathMode + 4]) << 8;
 
-  uint32 *dst = &ppu->renderBuffer[(y - 1) * 1024];
+  uint32 *dst = (uint32*)&ppu->renderBuffer[(y - 1) * 2 * ppu->renderPitch];
   
   uint32 windex = 0;
   do {
@@ -822,6 +926,10 @@
       } while (dst += 2, ++i < right);
     }
   } while (cw_clip_math >>= 1, ++windex < cwin.nr);
+
+  // Duplicate one line
+  memcpy((uint8*)(dst - 512) + ppu->renderPitch, dst - 512, 512 * 4);
+
 }
 
 static void ppu_handlePixel(Ppu* ppu, int x, int y) {
@@ -877,7 +985,7 @@
     }
   }
   int row = y - 1;
-  uint8 *pixelBuffer = (uint8*) &ppu->renderBuffer[row * 1024 + x * 2];
+  uint8 *pixelBuffer = (uint8*) &ppu->renderBuffer[row * 2 * ppu->renderPitch + x * 8];
   pixelBuffer[0] = 0;
   pixelBuffer[1] = ((b2 << 3) | (b2 >> 2)) * ppu->brightness / 15;
   pixelBuffer[2] = ((g2 << 3) | (g2 >> 2)) * ppu->brightness / 15;
--- a/snes/ppu.h
+++ b/snes/ppu.h
@@ -41,12 +41,22 @@
   uint8_t prio[256];
 } PpuPixelPrioBufs;
 
+enum {
+  kPpuRenderFlags_NewRenderer = 1,
+  // Render mode7 upsampled by 4x4
+  kPpuRenderFlags_4x4Mode7 = 2,
+};
+
+
 struct Ppu {
-  bool newRenderer;
   bool lineHasSprites;
   uint8_t lastBrightnessMult;
   uint8_t lastMosaicModulo;
-  uint32_t *renderBuffer;
+  uint8_t renderFlags;
+  uint32_t renderPitch;
+  uint8_t *renderBuffer;
+  float mode7PerspectiveLow, mode7PerspectiveHigh;
+
   Snes* snes;
   // store 31 extra entries to remove the need for clamp
   uint8_t brightnessMult[32 + 31]; 
@@ -139,6 +149,7 @@
   uint8_t ppu2openBus;
 
   uint8_t mosaicModulo[256];
+  uint32_t colorMapRgb[256];
 };
 
 Ppu* ppu_init(Snes* snes);
@@ -149,6 +160,8 @@
 uint8_t ppu_read(Ppu* ppu, uint8_t adr);
 void ppu_write(Ppu* ppu, uint8_t adr, uint8_t val);
 void ppu_saveload(Ppu *ppu, SaveLoadFunc *func, void *ctx);
-void PpuBeginDrawing(Ppu *ppu, uint32_t *buffer);
+bool PpuBeginDrawing(Ppu *ppu, uint8_t *buffer, size_t pitch, uint32_t render_flags);
+
+void PpuSetMode7PerspectiveCorrection(Ppu *ppu, int low, int high);
 
 #endif
--- a/zelda3.ini
+++ b/zelda3.ini
@@ -1,3 +1,8 @@
+[Graphics]
+NewRenderer = 1
+EnhancedMode7 = 1
+
+
 [KeyMap]
 # Change what keyboard keys map to the joypad
 # Order: Up, Down, Left, Right, Select, Start, A, B, X, Y, L, R
--- a/zelda3.vcxproj.filters
+++ b/zelda3.vcxproj.filters
@@ -31,9 +31,6 @@
     <ClCompile Include="snes\input.c">
       <Filter>Snes</Filter>
     </ClCompile>
-    <ClCompile Include="main.c">
-      <Filter>Snes</Filter>
-    </ClCompile>
     <ClCompile Include="snes\ppu.c">
       <Filter>Snes</Filter>
     </ClCompile>
@@ -120,6 +117,9 @@
     </ClCompile>
     <ClCompile Include="platform\win32\volume_control.c">
       <Filter>Platform</Filter>
+    </ClCompile>
+    <ClCompile Include="main.c">
+      <Filter>Zelda</Filter>
     </ClCompile>
   </ItemGroup>
   <ItemGroup>
--- a/zelda_rtl.c
+++ b/zelda_rtl.c
@@ -101,9 +101,9 @@
   case 0xCFA94: return kAttractDmaTable1;
   case 0xebd53: return kHdmaTableForEnding;
   case 0x0F2FB: return kSpotlightIndirectHdma;
-  case 0xabdcf: return kMapModeHdma0;
-  case 0xabdd6: return kMapModeHdma1;
-  case 0xABDDD: return kAttractIndirectHdmaTab;
+  case 0xabdcf: return kMapModeHdma0;             // mode7
+  case 0xabdd6: return kMapModeHdma1;             // mode7
+  case 0xABDDD: return kAttractIndirectHdmaTab;   // mode7
   case 0x2c80c: return kHdmaTableForPrayingScene;
 
   case 0x1b00: return (uint8 *)mode7_hdma_table;
@@ -161,10 +161,10 @@
   c->rep_count--;
 }
 
-void ZeldaDrawPpuFrame(uint32 *pixel_buffer) {
+bool ZeldaDrawPpuFrame(uint8 *pixel_buffer, size_t pitch, uint32 render_flags) {
   SimpleHdma hdma_chans[2];
 
-  PpuBeginDrawing(g_zenv.ppu, pixel_buffer);
+  bool rv = PpuBeginDrawing(g_zenv.ppu, pixel_buffer, pitch, render_flags);
 
   dma_startDma(g_zenv.dma, HDMAEN_copy, true);
 
@@ -171,6 +171,18 @@
   SimpleHdma_Init(&hdma_chans[0], &g_zenv.dma->channel[6]);
   SimpleHdma_Init(&hdma_chans[1], &g_zenv.dma->channel[7]);
 
+  // Cheat: Let the PPU impl know about the hdma perspective correction so it can avoid guessing.
+  if ((render_flags & kPpuRenderFlags_4x4Mode7) && g_zenv.ppu->mode == 7) {
+    if (hdma_chans[0].table == kMapModeHdma0)
+      PpuSetMode7PerspectiveCorrection(g_zenv.ppu, kMapMode_Zooms1[0], kMapMode_Zooms1[223]);
+    else if (hdma_chans[0].table == kMapModeHdma1)
+      PpuSetMode7PerspectiveCorrection(g_zenv.ppu, kMapMode_Zooms2[0], kMapMode_Zooms2[223]);
+    else if (hdma_chans[0].table == kAttractIndirectHdmaTab)
+      PpuSetMode7PerspectiveCorrection(g_zenv.ppu, mode7_hdma_table[0], mode7_hdma_table[223]);
+    else
+      PpuSetMode7PerspectiveCorrection(g_zenv.ppu, 0, 0);
+  }
+
   for (int i = 0; i < 225; i++) {
     if (i == 128 && irq_flag) {
       zelda_ppu_write(BG3HOFS, selectfile_var8);
@@ -186,6 +198,8 @@
     SimpleHdma_DoLine(&hdma_chans[0]);
     SimpleHdma_DoLine(&hdma_chans[1]);
   }
+
+  return rv;
 }
 
 void HdmaSetup(uint32 addr6, uint32 addr7, uint8 transfer_unit, uint8 reg6, uint8 reg7, uint8 indirect_bank) {
--- a/zelda_rtl.h
+++ b/zelda_rtl.h
@@ -184,8 +184,9 @@
 void zelda_ppu_write_word(uint32_t adr, uint16_t val);
 void zelda_apu_runcycles();
 const uint8 *SimpleHdma_GetPtr(uint32 p);
-// 512x480 32-bit pixels
-void ZeldaDrawPpuFrame(uint32 *pixel_buffer);
+
+// 512x480 32-bit pixels. Returns true if we instead draw 1024x960
+bool ZeldaDrawPpuFrame(uint8 *pixel_buffer, size_t pitch, uint32 render_flags);
 void HdmaSetup(uint32 addr6, uint32 addr7, uint8 transfer_unit, uint8 reg6, uint8 reg7, uint8 indirect_bank);
 void ZeldaInitializationCode();
 void ZeldaRunGameLoop();