mirror of
https://github.com/SourMesen/Mesen2.git
synced 2025-04-02 10:21:44 -04:00
Super Mario Advance 4 (SMB3) window animation when entering a stage had a glitchy scanline when the window's end position was moved from x=255 to x=239 during hblank, because ProcessWindow was not getting called when the window X position register was changed
1626 lines
50 KiB
C++
1626 lines
50 KiB
C++
#include "pch.h"
|
|
#include "GBA/GbaPpu.h"
|
|
#include "GBA/GbaTypes.h"
|
|
#include "GBA/GbaConsole.h"
|
|
#include "GBA/GbaMemoryManager.h"
|
|
#include "GBA/GbaDmaController.h"
|
|
#include "GBA/Debugger/GbaPpuTools.h"
|
|
#include "Debugger/DebugTypes.h"
|
|
#include "Shared/Emulator.h"
|
|
#include "Shared/EmuSettings.h"
|
|
#include "Shared/BaseControlManager.h"
|
|
#include "Shared/RewindManager.h"
|
|
#include "Shared/MessageManager.h"
|
|
#include "Shared/RenderedFrame.h"
|
|
#include "Shared/Video/VideoDecoder.h"
|
|
#include "Shared/Video/VideoRenderer.h"
|
|
#include "Shared/EventType.h"
|
|
#include "Shared/NotificationManager.h"
|
|
#include "Utilities/BitUtilities.h"
|
|
#include "Utilities/Serializer.h"
|
|
#include "Utilities/StaticFor.h"
|
|
|
|
void GbaPpu::Init(Emulator* emu, GbaConsole* console, GbaMemoryManager* memoryManager)
|
|
{
|
|
_emu = emu;
|
|
_console = console;
|
|
_memoryManager = memoryManager;
|
|
|
|
_state = {};
|
|
|
|
_paletteRam = (uint16_t*)_emu->GetMemory(MemoryType::GbaPaletteRam).Memory;
|
|
_vram = (uint8_t*)_emu->GetMemory(MemoryType::GbaVideoRam).Memory;
|
|
_vram16 = (uint16_t*)_emu->GetMemory(MemoryType::GbaVideoRam).Memory;
|
|
_oam = (uint32_t*)_emu->GetMemory(MemoryType::GbaSpriteRam).Memory;
|
|
|
|
_outputBuffers[0] = new uint16_t[GbaConstants::PixelCount];
|
|
_outputBuffers[1] = new uint16_t[GbaConstants::PixelCount];
|
|
memset(_outputBuffers[0], 0, GbaConstants::PixelCount * sizeof(uint16_t));
|
|
memset(_outputBuffers[1], 0, GbaConstants::PixelCount * sizeof(uint16_t));
|
|
_currentBuffer = _outputBuffers[0];
|
|
|
|
_oamReadOutput = _oamOutputBuffers[0];
|
|
_oamWriteOutput = _oamOutputBuffers[1];
|
|
|
|
if(_emu->GetSettings()->GetGbaConfig().SkipBootScreen) {
|
|
//BIOS leaves PPU registers in this state, some games expect this
|
|
_state.Control = 0x80;
|
|
_state.ForcedBlank = true;
|
|
_state.Transform[0].Matrix[0] = 0x100;
|
|
_state.Transform[0].Matrix[3] = 0x100;
|
|
_state.Transform[1].Matrix[0] = 0x100;
|
|
_state.Transform[1].Matrix[3] = 0x100;
|
|
}
|
|
|
|
//All layers are always active when no window is enabled
|
|
for(int i = 0; i < 6; i++) {
|
|
_state.WindowActiveLayers[4][i] = true;
|
|
}
|
|
|
|
StaticFor<0, 128>::Apply([=](auto i) {
|
|
_colorMathFunc[i] = &GbaPpu::ProcessColorMath<(GbaPpuBlendEffect)(i >> 5), (bool)(i & 0x01), (bool)(i & 0x02), (bool)(i & 0x04), (bool)(i & 0x08), (bool)(i & 0x10)>;
|
|
});
|
|
}
|
|
|
|
GbaPpu::~GbaPpu()
|
|
{
|
|
delete[] _outputBuffers[0];
|
|
delete[] _outputBuffers[1];
|
|
}
|
|
|
|
void GbaPpu::ProcessHBlank()
|
|
{
|
|
if(_state.Scanline < 160) {
|
|
RenderScanline();
|
|
_console->GetDmaController()->TriggerDma(GbaDmaTrigger::HBlank);
|
|
}
|
|
|
|
for(int i = 0; i < 4; i++) {
|
|
if(_state.BgLayers[i].EnableTimer && --_state.BgLayers[i].EnableTimer == 0) {
|
|
//Exact timing hasn't been verified
|
|
//The mGBA Suite test writes on H=1065 and expects the layer to be enabled 3 scanlines later (write on scanline 101, starts rendering on scanline 104, so just over 2 scanlines)
|
|
//Spyro - Season of Ice writes on H=30, and expects the layer to be enabled 2 scanlines later
|
|
//Doing this at the start of hblank allows both scenarios the work
|
|
_state.BgLayers[i].Enabled = true;
|
|
}
|
|
}
|
|
|
|
if(_state.HblankIrqEnabled) {
|
|
_console->GetMemoryManager()->SetDelayedIrqSource(GbaIrqSource::LcdHblank, 4);
|
|
}
|
|
}
|
|
|
|
void GbaPpu::ProcessEndOfScanline()
|
|
{
|
|
ProcessSprites();
|
|
ProcessWindow();
|
|
|
|
if(!_skipRender && _emu->IsDebugging()) {
|
|
DebugProcessMemoryAccessView();
|
|
}
|
|
|
|
memset(_memoryAccess, GbaPpuMemAccess::None, sizeof(_memoryAccess));
|
|
|
|
_state.Cycle = 0;
|
|
_state.Scanline++;
|
|
|
|
//Reset renderer data for next scanline
|
|
_lastRenderCycle = -1;
|
|
_lastWindowCycle = -1;
|
|
_oamLastCycle = -1;
|
|
std::fill(_layerOutput[0], _layerOutput[0] + 240, GbaPixelData {});
|
|
std::fill(_layerOutput[1], _layerOutput[1] + 240, GbaPixelData {});
|
|
std::fill(_layerOutput[2], _layerOutput[2] + 240, GbaPixelData {});
|
|
std::fill(_layerOutput[3], _layerOutput[3] + 240, GbaPixelData {});
|
|
|
|
for(int i = 0; i < 2; i++) {
|
|
if(_state.Transform[i].PendingUpdateX) {
|
|
_state.Transform[i].LatchOriginX = (_state.Transform[i].OriginX << 4) >> 4; //sign extend
|
|
_state.Transform[i].PendingUpdateX = false;
|
|
}
|
|
if(_state.Transform[i].PendingUpdateY) {
|
|
_state.Transform[i].LatchOriginY = (_state.Transform[i].OriginY << 4) >> 4; //sign extend
|
|
_state.Transform[i].PendingUpdateY = false;
|
|
}
|
|
}
|
|
|
|
for(int i = 0; i < 4; i++) {
|
|
//Unverified: Latch X scroll value at the start of each scanline
|
|
//This fixes display issues in the Fire Emblem Sacred Stones menu
|
|
_state.BgLayers[i].ScrollXLatch = _state.BgLayers[i].ScrollX;
|
|
}
|
|
|
|
if(_state.Scanline >= 2 && _state.Scanline < 162 && _triggerSpecialDma) {
|
|
//"Video Capture Mode" dma, channel 3 only - auto-stops on scanline 161
|
|
_console->GetDmaController()->TriggerDmaChannel(GbaDmaTrigger::Special, 3, _state.Scanline == 161);
|
|
} else if(_state.Scanline == 162) {
|
|
_triggerSpecialDma = _console->GetDmaController()->IsVideoCaptureDmaEnabled();
|
|
}
|
|
|
|
if(_state.Scanline == 160) {
|
|
_oamScanline = 0;
|
|
_state.ObjEnableTimer = 0;
|
|
SendFrame();
|
|
if(_state.VblankIrqEnabled) {
|
|
_console->GetMemoryManager()->SetIrqSource(GbaIrqSource::LcdVblank);
|
|
}
|
|
_console->GetDmaController()->TriggerDma(GbaDmaTrigger::VBlank);
|
|
} else if(_state.Scanline == 228) {
|
|
_state.Scanline = 0;
|
|
|
|
//Transform values latched at the start of the frame
|
|
_state.Transform[0].LatchOriginX = (int32_t)_state.Transform[0].OriginX;
|
|
_state.Transform[0].LatchOriginY = (int32_t)_state.Transform[0].OriginY;
|
|
_state.Transform[1].LatchOriginX = (int32_t)_state.Transform[1].OriginX;
|
|
_state.Transform[1].LatchOriginY = (int32_t)_state.Transform[1].OriginY;
|
|
|
|
if(_emu->GetSettings()->GetGbaConfig().DisableSprites) {
|
|
std::fill(_oamOutputBuffers[0], _oamOutputBuffers[0] + 240, GbaPixelData {});
|
|
std::fill(_oamOutputBuffers[1], _oamOutputBuffers[1] + 240, GbaPixelData {});
|
|
}
|
|
|
|
_emu->ProcessEvent(EventType::StartFrame, CpuType::Gba);
|
|
|
|
EmuSettings* settings = _emu->GetSettings();
|
|
_skipRender = (
|
|
!settings->GetGbaConfig().DisableFrameSkipping &&
|
|
!_emu->GetRewindManager()->IsRewinding() &&
|
|
!_emu->GetVideoRenderer()->IsRecording() &&
|
|
(settings->GetEmulationSpeed() == 0 || settings->GetEmulationSpeed() > 150) &&
|
|
_frameSkipTimer.GetElapsedMS() < 15
|
|
);
|
|
if(!_skipRender) {
|
|
_currentBuffer = _currentBuffer == _outputBuffers[0] ? _outputBuffers[1] : _outputBuffers[0];
|
|
}
|
|
}
|
|
|
|
if(_state.ScanlineIrqEnabled && _state.Scanline == _state.Lyc) {
|
|
_console->GetMemoryManager()->SetIrqSource(GbaIrqSource::LcdScanlineMatch);
|
|
}
|
|
|
|
InitializeWindows();
|
|
}
|
|
|
|
void GbaPpu::SendFrame()
|
|
{
|
|
_emu->ProcessEvent(EventType::EndFrame, CpuType::Gba);
|
|
_emu->GetNotificationManager()->SendNotification(ConsoleNotificationType::PpuFrameDone);
|
|
|
|
RenderedFrame frame(_currentBuffer, GbaConstants::ScreenWidth, GbaConstants::ScreenHeight, 1.0, _state.FrameCount, _console->GetControlManager()->GetPortStates());
|
|
bool rewinding = _emu->GetRewindManager()->IsRewinding();
|
|
_emu->GetVideoDecoder()->UpdateFrame(frame, rewinding, rewinding);
|
|
|
|
_emu->ProcessEndOfFrame();
|
|
_console->ProcessEndOfFrame();
|
|
|
|
_state.FrameCount++;
|
|
|
|
if(!_skipRender) {
|
|
_frameSkipTimer.Reset();
|
|
}
|
|
}
|
|
|
|
void GbaPpu::DebugSendFrame()
|
|
{
|
|
RenderScanline(true);
|
|
|
|
int lastDrawnPixel = std::clamp((_state.Cycle - 46) / 4, 0, 239);
|
|
|
|
int offset = lastDrawnPixel + 1 + _state.Scanline * GbaConstants::ScreenWidth;
|
|
int pixelsToClear = GbaConstants::ScreenWidth * GbaConstants::ScreenHeight - offset;
|
|
if(pixelsToClear > 0) {
|
|
memset(_currentBuffer + offset, 0, pixelsToClear * sizeof(uint16_t));
|
|
}
|
|
|
|
RenderedFrame frame(_currentBuffer, GbaConstants::ScreenWidth, GbaConstants::ScreenHeight, 1.0, _state.FrameCount);
|
|
_emu->GetVideoDecoder()->UpdateFrame(frame, false, false);
|
|
}
|
|
|
|
void GbaPpu::RenderScanline(bool forceRender)
|
|
{
|
|
if(_skipRender && !forceRender) {
|
|
return;
|
|
}
|
|
|
|
ProcessSprites();
|
|
ProcessWindow();
|
|
|
|
if(_state.Scanline >= 160 || _lastRenderCycle >= 1006) {
|
|
return;
|
|
}
|
|
|
|
if(_state.ForcedBlank) {
|
|
uint16_t* rowStart = _currentBuffer + (_state.Scanline * GbaConstants::ScreenWidth);
|
|
std::fill(rowStart, rowStart + GbaConstants::ScreenWidth, 0x7FFF);
|
|
return;
|
|
}
|
|
|
|
uint8_t activeLayers = 0;
|
|
switch(_state.BgMode) {
|
|
case 0:
|
|
RenderTilemap<0>();
|
|
RenderTilemap<1>();
|
|
RenderTilemap<2>();
|
|
RenderTilemap<3>();
|
|
activeLayers = _state.Control2 & 0x0F;
|
|
break;
|
|
|
|
case 1:
|
|
RenderTilemap<0>();
|
|
RenderTilemap<1>();
|
|
RenderTransformTilemap<2>();
|
|
activeLayers = _state.Control2 & 0x07;
|
|
break;
|
|
|
|
case 2:
|
|
RenderTransformTilemap<2>();
|
|
RenderTransformTilemap<3>();
|
|
activeLayers = _state.Control2 & 0x0C;
|
|
break;
|
|
|
|
case 3: RenderBitmapMode<3>(); activeLayers = _state.Control2 & 0x04; break;
|
|
case 4: RenderBitmapMode<4>(); activeLayers = _state.Control2 & 0x04; break;
|
|
case 5: RenderBitmapMode<5>(); activeLayers = _state.Control2 & 0x04; break;
|
|
|
|
default: break;
|
|
}
|
|
|
|
if(_state.Cycle >= 46) {
|
|
bool windowEnabled = _state.Window0Enabled || _state.Window1Enabled || _state.ObjWindowEnabled;
|
|
(this->*_colorMathFunc[((int)_state.BlendEffect << 5) | ((int)windowEnabled << 4) | activeLayers])();
|
|
}
|
|
|
|
_lastRenderCycle = _state.Cycle;
|
|
}
|
|
|
|
template<GbaPpuBlendEffect effect, bool bg0Enabled, bool bg1Enabled, bool bg2Enabled, bool bg3Enabled, bool windowEnabled>
|
|
void GbaPpu::ProcessColorMath()
|
|
{
|
|
uint16_t* dst = _skipRender ? _skippedOutput : (_currentBuffer + (_state.Scanline * GbaConstants::ScreenWidth));
|
|
uint8_t mainCoeff = std::min<uint8_t>(16, _state.BlendMainCoefficient);
|
|
uint8_t subCoeff = std::min<uint8_t>(16, _state.BlendSubCoefficient);
|
|
|
|
GbaPixelData main = {};
|
|
GbaPixelData sub = {};
|
|
uint8_t brightness = std::min<uint8_t>(16, _state.Brightness);
|
|
uint16_t blendColor = effect == GbaPpuBlendEffect::IncreaseBrightness ? GbaPpu::WhiteColor : GbaPpu::BlackColor;
|
|
|
|
uint8_t wnd = 0;
|
|
|
|
int start = _lastRenderCycle < 46 ? 0 : std::max(0, ((_lastRenderCycle - 46) / 4) + 1);
|
|
int end = std::min((_state.Cycle - 46) / 4, 239);
|
|
|
|
GbaPixelData sprPixel = {};
|
|
|
|
for(int x = start; x <= end; x++) {
|
|
|
|
if constexpr(windowEnabled) {
|
|
wnd = _activeWindow[x];
|
|
if(_state.WindowActiveLayers[wnd][GbaPpu::SpriteLayerIndex]) {
|
|
main = _oamReadOutput[x];
|
|
} else {
|
|
main = {};
|
|
}
|
|
} else {
|
|
wnd = GbaPpu::NoWindow;
|
|
main = _oamReadOutput[x];
|
|
}
|
|
|
|
if(!(main.Color & GbaPpu::SpriteMosaicFlag) || !(sprPixel.Color & GbaPpu::SpriteMosaicFlag) || x % (_state.ObjMosaicSizeX + 1) == 0) {
|
|
sprPixel = main;
|
|
} else {
|
|
main = sprPixel;
|
|
}
|
|
|
|
sub = {};
|
|
|
|
if constexpr(bg0Enabled) {
|
|
ProcessLayerPixel<0, windowEnabled>(x, wnd, main, sub);
|
|
}
|
|
if constexpr(bg1Enabled) {
|
|
ProcessLayerPixel<1, windowEnabled>(x, wnd, main, sub);
|
|
}
|
|
if constexpr(bg2Enabled) {
|
|
ProcessLayerPixel<2, windowEnabled>(x, wnd, main, sub);
|
|
}
|
|
if constexpr(bg3Enabled) {
|
|
ProcessLayerPixel<3, windowEnabled>(x, wnd, main, sub);
|
|
}
|
|
|
|
if((main.Color & (GbaPpu::SpriteBlendFlag | GbaPpu::DirectColorFlag)) == GbaPpu::SpriteBlendFlag && _state.BlendSub[sub.Layer]) {
|
|
//Sprite transparency is applied before anything else
|
|
BlendColors(dst, x, ReadColor<false>(x, main.Color), mainCoeff, ReadColor<true>(x, sub.Color), subCoeff);
|
|
} else {
|
|
if constexpr(effect == GbaPpuBlendEffect::None) {
|
|
dst[x] = ReadColor<false>(x, main.Color);
|
|
} else if constexpr(effect == GbaPpuBlendEffect::AlphaBlend) {
|
|
if(_state.BlendSub[sub.Layer]) {
|
|
if(!_state.BlendMain[main.Layer] || !_state.WindowActiveLayers[wnd][GbaPpu::EffectLayerIndex]) {
|
|
dst[x] = ReadColor<false>(x, main.Color);
|
|
} else {
|
|
BlendColors(dst, x, ReadColor<false>(x, main.Color), mainCoeff, ReadColor<true>(x, sub.Color), subCoeff);
|
|
}
|
|
} else {
|
|
dst[x] = ReadColor<false>(x, main.Color);
|
|
}
|
|
} else {
|
|
if(brightness == 0 || !_state.BlendMain[main.Layer] || !_state.WindowActiveLayers[wnd][GbaPpu::EffectLayerIndex]) {
|
|
dst[x] = ReadColor<false>(x, main.Color);
|
|
} else {
|
|
BlendColors(dst, x, ReadColor<false>(x, main.Color), 16 - brightness, blendColor, brightness);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(_state.GreenSwapEnabled) {
|
|
for(int x = start & ~1; x + 1 <= end; x+=2) {
|
|
uint16_t gLeft = dst[x] & 0x3E0;
|
|
uint16_t gRight = dst[x+1] & 0x3E0;
|
|
dst[x] = (dst[x] & ~0x3E0) | gRight;
|
|
dst[x+1] = (dst[x+1] & ~0x3E0) | gLeft;
|
|
}
|
|
}
|
|
}
|
|
|
|
template<bool isSubColor>
|
|
uint16_t GbaPpu::ReadColor(int x, uint16_t addr)
|
|
{
|
|
if(addr & GbaPpu::DirectColorFlag) {
|
|
return addr & 0x7FFF;
|
|
} else {
|
|
_memoryAccess[(x << 2) + (isSubColor ? 48 : 46)] |= GbaPpuMemAccess::Palette;
|
|
return _paletteRam[(addr >> 1) & 0x1FF] & 0x7FFF;
|
|
}
|
|
}
|
|
|
|
void GbaPpu::BlendColors(uint16_t* dst, int x, uint16_t main, uint8_t aCoeff, uint16_t sub, uint8_t bCoeff)
|
|
{
|
|
uint8_t aR = main & 0x1F;
|
|
uint8_t aG = (main >> 5) & 0x1F;
|
|
uint8_t aB = (main >> 10) & 0x1F;
|
|
|
|
uint8_t bR = sub & 0x1F;
|
|
uint8_t bG = (sub >> 5) & 0x1F;
|
|
uint8_t bB = (sub >> 10) & 0x1F;
|
|
|
|
uint32_t r = std::min(31, (aR * aCoeff + bR * bCoeff) >> 4);
|
|
uint32_t g = std::min(31, (aG * aCoeff + bG * bCoeff) >> 4);
|
|
uint32_t b = std::min(31, (aB * aCoeff + bB * bCoeff) >> 4);
|
|
|
|
dst[x] = r | (g << 5) | (b << 10);
|
|
}
|
|
|
|
void GbaPpu::InitializeWindows()
|
|
{
|
|
//Windows are enabled/disabled when the scanline reaches the start/end scanlines
|
|
//See window_midframe test - unsure about behavior when top==bottom
|
|
if(_state.Scanline == _state.Window[0].TopY) {
|
|
_window0ActiveY = _state.Window0Enabled;
|
|
}
|
|
if(_state.Scanline == _state.Window[0].BottomY) {
|
|
_window0ActiveY = false;
|
|
}
|
|
|
|
if(_state.Scanline == _state.Window[1].TopY) {
|
|
_window1ActiveY = _state.Window1Enabled;
|
|
}
|
|
if(_state.Scanline == _state.Window[1].BottomY) {
|
|
_window1ActiveY = false;
|
|
}
|
|
|
|
memset(_activeWindow, GbaPpu::OutsideWindow, sizeof(_activeWindow));
|
|
}
|
|
|
|
void GbaPpu::ProcessWindow()
|
|
{
|
|
//Using the same logic as for window Y above, enable/disable windows when
|
|
//the current pixel matches the left/right X value for the window
|
|
//Some games set left > right, essentially making the window
|
|
//wrap around to the beginning of the next scanline.
|
|
//(MM&B does this for speech bubbles)
|
|
//TODOGBA is there any test rom for this (window x)?
|
|
int x = (_lastWindowCycle + 1) / 4;
|
|
int end = std::min(_state.Cycle / 4, 255) + 1;
|
|
if(x >= end) {
|
|
return;
|
|
}
|
|
|
|
if(_state.Window0Enabled || _state.Window1Enabled) {
|
|
for(int i = -1; i < 4; i++) {
|
|
uint16_t changePos = i < 0 ? 0 : _windowChangePos[i];
|
|
uint16_t nextChange = i < 3 ? _windowChangePos[i + 1] : 256;
|
|
if(nextChange == changePos || (changePos < x && nextChange < x)) {
|
|
continue;
|
|
}
|
|
|
|
if(x == _state.Window[0].LeftX) {
|
|
_window0ActiveX = _state.Window0Enabled;
|
|
}
|
|
if(x == _state.Window[0].RightX) {
|
|
_window0ActiveX = false;
|
|
}
|
|
|
|
if(x == _state.Window[1].LeftX) {
|
|
_window1ActiveX = _state.Window1Enabled;
|
|
}
|
|
if(x == _state.Window[1].RightX) {
|
|
_window1ActiveX = false;
|
|
}
|
|
|
|
int length = std::min(nextChange - x, end - x);
|
|
if(_window0ActiveX && _window0ActiveY) {
|
|
memset(_activeWindow + x, GbaPpu::Window0, length);
|
|
} else if(_window1ActiveX && _window1ActiveY) {
|
|
memset(_activeWindow + x, GbaPpu::Window1, length);
|
|
}
|
|
|
|
x += length;
|
|
if(x >= end) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
_window0ActiveX = false;
|
|
_window1ActiveX = false;
|
|
}
|
|
|
|
_lastWindowCycle = _state.Cycle;
|
|
}
|
|
|
|
void GbaPpu::SetWindowX(uint8_t& regValue, uint8_t newValue)
|
|
{
|
|
if(regValue != newValue) {
|
|
regValue = newValue;
|
|
UpdateWindowChangePoints();
|
|
}
|
|
}
|
|
|
|
void GbaPpu::UpdateWindowChangePoints()
|
|
{
|
|
//Generate a sorted list of the positions where the windows start/end
|
|
//Used in ProcessWindow to process the changes in chunks
|
|
_windowChangePos[0] = _state.Window[0].LeftX;
|
|
_windowChangePos[1] = _state.Window[0].RightX;
|
|
_windowChangePos[2] = _state.Window[1].LeftX;
|
|
_windowChangePos[3] = _state.Window[1].RightX;
|
|
std::sort(_windowChangePos, _windowChangePos + 4);
|
|
}
|
|
|
|
void GbaPpu::SetPixelData(GbaPixelData& pixel, uint16_t color, uint8_t priority, uint8_t layer)
|
|
{
|
|
pixel.Color = color;
|
|
pixel.Priority = priority;
|
|
pixel.Layer = layer;
|
|
}
|
|
|
|
template<int i, bool mosaic, bool bpp8>
|
|
void GbaPpu::PushBgPixels()
|
|
{
|
|
if(_layerData[i].HoriMirror) {
|
|
PushBgPixels<i, mosaic, bpp8, true>();
|
|
} else {
|
|
PushBgPixels<i, mosaic, bpp8, false>();
|
|
}
|
|
}
|
|
|
|
template<int i, bool mosaic, bool bpp8, bool mirror>
|
|
void GbaPpu::PushBgPixels()
|
|
{
|
|
GbaLayerRendererData& data = _layerData[i];
|
|
|
|
uint16_t tileData = _vram16[data.FetchAddr >> 1];
|
|
if constexpr(mirror) {
|
|
if constexpr(bpp8) {
|
|
tileData = ((tileData & 0xFF00) >> 8) | ((tileData & 0x00FF) << 8);
|
|
} else {
|
|
tileData = (
|
|
((tileData & 0xF000) >> 12) |
|
|
((tileData & 0x0F00) >> 4) |
|
|
((tileData & 0x00F0) << 4) |
|
|
((tileData & 0x000F) << 12)
|
|
);
|
|
}
|
|
}
|
|
|
|
int16_t renderX = data.RenderX;
|
|
constexpr int len = bpp8 ? 2 : 4;
|
|
for(int x = 0; x < len; x++) {
|
|
if(renderX >= 0 && renderX < 240) {
|
|
if constexpr(mosaic) {
|
|
if(renderX % (_state.BgMosaicSizeX + 1) == 0) {
|
|
data.MosaicColor = tileData & (bpp8 ? 0xFF : 0x0F);
|
|
}
|
|
} else {
|
|
data.MosaicColor = tileData & (bpp8 ? 0xFF : 0x0F);
|
|
}
|
|
|
|
if(data.MosaicColor != 0) {
|
|
SetPixelData(_layerOutput[i][renderX], (data.PaletteIndex * 32) + data.MosaicColor * 2, _state.BgLayers[i].Priority, i);
|
|
}
|
|
}
|
|
tileData >>= bpp8 ? 8 : 4;
|
|
renderX++;
|
|
}
|
|
|
|
data.FetchAddr += (mirror ? -2 : 2);
|
|
data.RenderX = renderX;
|
|
};
|
|
|
|
template<int i>
|
|
void GbaPpu::RenderTilemap()
|
|
{
|
|
if(!_state.BgLayers[i].Enabled || _emu->GetSettings()->GetGbaConfig().HideBgLayers[i]) {
|
|
return;
|
|
}
|
|
|
|
if(_state.BgLayers[i].Mosaic) {
|
|
if(_state.BgLayers[i].Bpp8Mode) {
|
|
RenderTilemap<i, true, true>();
|
|
} else {
|
|
RenderTilemap<i, true, false>();
|
|
}
|
|
} else {
|
|
if(_state.BgLayers[i].Bpp8Mode) {
|
|
RenderTilemap<i, false, true>();
|
|
} else {
|
|
RenderTilemap<i, false, false>();
|
|
}
|
|
}
|
|
}
|
|
|
|
template<int i, bool mosaic, bool bpp8>
|
|
void GbaPpu::RenderTilemap()
|
|
{
|
|
GbaBgConfig& layer = _state.BgLayers[i];
|
|
|
|
uint16_t baseAddr = layer.TilemapAddr >> 1;
|
|
uint16_t scanline = _state.Scanline;
|
|
if constexpr(mosaic) {
|
|
scanline = scanline - scanline % (_state.BgMosaicSizeY + 1);
|
|
}
|
|
uint16_t yPos = layer.ScrollY + scanline;
|
|
|
|
if(layer.DoubleHeight && (yPos & 0x100)) {
|
|
baseAddr += layer.DoubleWidth ? 0x800 : 0x400;
|
|
}
|
|
|
|
yPos &= 0xFF;
|
|
|
|
if(_lastRenderCycle == -1) {
|
|
_layerData[i].RenderX = -(layer.ScrollXLatch & 0x07);
|
|
}
|
|
|
|
//MessageManager::Log(std::to_string(_state.Scanline) + " render " + std::to_string(_lastRenderCycle+1) + " to " + std::to_string(_state.Cycle));
|
|
int gap = (31 + i - (layer.ScrollXLatch & 0x07) * 4);
|
|
int cycle = std::max(0, _lastRenderCycle + 1 - gap);
|
|
int end = std::min<int>(_state.Cycle, 1005) - gap;
|
|
|
|
for(; cycle <= end; cycle++) {
|
|
//MessageManager::Log(std::to_string(_state.Scanline) + " fetch cycle " + std::to_string(fetchCycle));
|
|
switch(cycle & 0x1F) {
|
|
case 0: {
|
|
//Fetch tilemap data
|
|
uint16_t xPos = layer.ScrollXLatch + _layerData[i].RenderX;
|
|
uint16_t addr = baseAddr;
|
|
if(layer.DoubleWidth && (xPos & 0x100)) {
|
|
addr += 0x400;
|
|
}
|
|
xPos &= 0xFF;
|
|
|
|
uint16_t vramAddr = addr + yPos / 8 * 32 + xPos / 8;
|
|
uint16_t tilemapData = _vram16[vramAddr];
|
|
_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;
|
|
|
|
_layerData[i].TileIndex = tilemapData & 0x3FF;
|
|
_layerData[i].HoriMirror = tilemapData & (1 << 10);
|
|
_layerData[i].VertMirror = tilemapData & (1 << 11);
|
|
_layerData[i].PaletteIndex = bpp8 ? 0 : (tilemapData >> 12) & 0x0F;
|
|
|
|
_layerData[i].TileRow = _layerData[i].VertMirror ? (~yPos & 0x07) : (yPos & 0x07);
|
|
_layerData[i].TileColumn = _layerData[i].HoriMirror ? (bpp8 ? 6 : 2) : 0;
|
|
_layerData[i].FetchAddr = layer.TilesetAddr + _layerData[i].TileIndex * (bpp8 ? 64 : 32) + _layerData[i].TileRow * (bpp8 ? 8 : 4) + _layerData[i].TileColumn;
|
|
|
|
//MessageManager::Log(std::to_string(_state.Scanline) + "," + std::to_string(cycle) + " fetch tilemap");
|
|
cycle += 3;
|
|
break;
|
|
}
|
|
|
|
case 4: {
|
|
//Fetch tile data (4bpp & 8bpp)
|
|
PushBgPixels<i, mosaic, bpp8>();
|
|
_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;
|
|
|
|
//MessageManager::Log(std::to_string(_state.Scanline) + "," + std::to_string(cycle) + " fetch tile data 0");
|
|
cycle += bpp8 ? 7 : 15;
|
|
break;
|
|
}
|
|
|
|
case 12: {
|
|
//Fetch tile data (8bpp only)
|
|
if constexpr(bpp8) {
|
|
PushBgPixels<i, mosaic, bpp8>();
|
|
_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;
|
|
}
|
|
cycle += 7;
|
|
break;
|
|
}
|
|
|
|
case 20: {
|
|
//Fetch tile data (4bpp & 8bpp)
|
|
PushBgPixels<i, mosaic, bpp8>();
|
|
_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;
|
|
|
|
//MessageManager::Log(std::to_string(_state.Scanline) + "," + std::to_string(cycle) + " fetch tile data 2");
|
|
cycle += bpp8 ? 7 : (11 - i);
|
|
break;
|
|
}
|
|
|
|
case 28: {
|
|
//Fetch tile data (8bpp only)
|
|
if constexpr(bpp8) {
|
|
PushBgPixels<i, mosaic, bpp8>();
|
|
_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;
|
|
}
|
|
cycle += 3 - i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
template<int i>
|
|
void GbaPpu::RenderTransformTilemap()
|
|
{
|
|
GbaBgConfig& layer = _state.BgLayers[i];
|
|
GbaTransformConfig& cfg = _state.Transform[i - 2];
|
|
if(!layer.Enabled || _emu->GetSettings()->GetGbaConfig().HideBgLayers[i]) {
|
|
return;
|
|
}
|
|
|
|
uint16_t screenSize = 128 << layer.ScreenSize;
|
|
|
|
if(_lastRenderCycle == -1) {
|
|
_layerData[i].TransformX = (cfg.LatchOriginX << 4) >> 4; //sign extend
|
|
_layerData[i].TransformY = (cfg.LatchOriginY << 4) >> 4;
|
|
_layerData[i].RenderX = 0;
|
|
}
|
|
|
|
//MessageManager::Log(std::to_string(_state.Scanline) + " render " + std::to_string(_lastRenderCycle+1) + " to " + std::to_string(_state.Cycle));
|
|
constexpr int gap = (37 - i * 2);
|
|
int cycle = std::max(0, _lastRenderCycle + 1 - gap);
|
|
int end = std::min<int>(_state.Cycle, 1005) - gap;
|
|
|
|
for(; cycle <= end; cycle++) {
|
|
switch(cycle & 0x03) {
|
|
case 0: {
|
|
//Fetch tilemap data
|
|
uint32_t wrapMask = layer.WrapAround ? (screenSize - 1) : 0xFFFFF;
|
|
uint16_t columnCount = screenSize >> 3;
|
|
|
|
if(!layer.Mosaic || _layerData[i].RenderX % (_state.BgMosaicSizeX + 1) == 0) {
|
|
//Ignore decimal point value (bottom 8 bits), apply wraparound behavior
|
|
//This produces the x,y coordinate (on the tilemap) that needs to be drawn
|
|
_layerData[i].XPos = (_layerData[i].TransformX >> 8) & wrapMask;
|
|
_layerData[i].YPos = (_layerData[i].TransformY >> 8) & wrapMask;
|
|
}
|
|
|
|
uint16_t vramAddr = layer.TilemapAddr + (_layerData[i].YPos >> 3) * columnCount + (_layerData[i].XPos >> 3);
|
|
_layerData[i].TileIndex = _vram[vramAddr];
|
|
_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;
|
|
|
|
_layerData[i].TileRow = _layerData[i].YPos & 0x07;
|
|
_layerData[i].TileColumn = _layerData[i].XPos & 0x07;
|
|
|
|
//Update x/y values for next dot
|
|
_layerData[i].TransformX += cfg.Matrix[0];
|
|
_layerData[i].TransformY += cfg.Matrix[2];
|
|
//MessageManager::Log(std::to_string(_state.Scanline) + "," + std::to_string(cycle) + " fetch tilemap");
|
|
break;
|
|
}
|
|
|
|
case 1: {
|
|
//Fetch pixel
|
|
_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;
|
|
if(_layerData[i].RenderX < 240) {
|
|
uint8_t color = _vram[(layer.TilesetAddr + _layerData[i].TileIndex * 64 + _layerData[i].TileRow * 8 + _layerData[i].TileColumn) & 0xFFFF];
|
|
if(color != 0 && _layerData[i].XPos < screenSize && _layerData[i].YPos < screenSize) {
|
|
SetPixelData(_layerOutput[i][_layerData[i].RenderX], color * 2, layer.Priority, i);
|
|
}
|
|
}
|
|
_layerData[i].RenderX++;
|
|
//MessageManager::Log(std::to_string(_state.Scanline) + "," + std::to_string(cycle) + " fetch pixel");
|
|
cycle += 2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(_state.Cycle == 1006) {
|
|
//Update x/y values for next scanline
|
|
if(!layer.Mosaic) {
|
|
cfg.LatchOriginX += cfg.Matrix[1];
|
|
cfg.LatchOriginY += cfg.Matrix[3];
|
|
} else if(_state.Scanline % (_state.BgMosaicSizeY + 1) == _state.BgMosaicSizeY) {
|
|
cfg.LatchOriginX += cfg.Matrix[1] * (_state.BgMosaicSizeY + 1);
|
|
cfg.LatchOriginY += cfg.Matrix[3] * (_state.BgMosaicSizeY + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
template<int mode>
|
|
void GbaPpu::RenderBitmapMode()
|
|
{
|
|
GbaBgConfig& layer = _state.BgLayers[2];
|
|
if(!layer.Enabled || _emu->GetSettings()->GetGbaConfig().HideBgLayers[2]) {
|
|
return;
|
|
}
|
|
|
|
GbaTransformConfig& cfg = _state.Transform[0];
|
|
|
|
if(_lastRenderCycle == -1) {
|
|
_layerData[2].TransformX = (cfg.LatchOriginX << 4) >> 4; //sign extend
|
|
_layerData[2].TransformY = (cfg.LatchOriginY << 4) >> 4;
|
|
_layerData[2].RenderX = 0;
|
|
}
|
|
|
|
uint16_t screenWidth = mode == 5 ? 160 : 240;
|
|
uint16_t screenHeight = mode == 5 ? 128 : 160;
|
|
|
|
uint32_t base = (_state.DisplayFrameSelect && (mode == 4 || mode == 5)) ? 0xA000 : 0;
|
|
|
|
constexpr int gap = 34;
|
|
int cycle = std::max(0, _lastRenderCycle + 1 - gap);
|
|
int end = std::min<int>(_state.Cycle, 1005) - gap;
|
|
|
|
for(; cycle <= end; cycle++) {
|
|
if(!(cycle & 0x03)) {
|
|
if(!layer.Mosaic || _layerData[2].RenderX % (_state.BgMosaicSizeX + 1) == 0) {
|
|
//Ignore decimal point value (bottom 8 bits), apply wraparound behavior
|
|
//This produces the x,y coordinate (on the tilemap) that needs to be drawn
|
|
_layerData[2].XPos = (_layerData[2].TransformX >> 8);
|
|
_layerData[2].YPos = (_layerData[2].TransformY >> 8);
|
|
}
|
|
|
|
_memoryAccess[cycle+gap] |= GbaPpuMemAccess::Vram;
|
|
|
|
if(_layerData[2].YPos < screenHeight && _layerData[2].XPos < screenWidth) {
|
|
uint32_t addr = _layerData[2].YPos * screenWidth + _layerData[2].XPos;
|
|
if constexpr(mode == 3 || mode == 5) {
|
|
SetPixelData(_layerOutput[2][_layerData[2].RenderX], _vram16[(base >> 1) + addr] | GbaPpu::DirectColorFlag, layer.Priority, 2);
|
|
} else if constexpr(mode == 4) {
|
|
uint8_t color = _vram[base + addr];
|
|
if(color != 0) {
|
|
SetPixelData(_layerOutput[2][_layerData[2].RenderX], color * 2, layer.Priority, 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Update x/y values for next dot
|
|
_layerData[2].TransformX += cfg.Matrix[0];
|
|
_layerData[2].TransformY += cfg.Matrix[2];
|
|
_layerData[2].RenderX++;
|
|
cycle += 2;
|
|
}
|
|
}
|
|
|
|
if(_state.Cycle == 1006) {
|
|
//Update x/y values for next scanline
|
|
if(!layer.Mosaic) {
|
|
cfg.LatchOriginX += cfg.Matrix[1];
|
|
cfg.LatchOriginY += cfg.Matrix[3];
|
|
} else if(_state.Scanline % (_state.BgMosaicSizeY + 1) == _state.BgMosaicSizeY) {
|
|
cfg.LatchOriginX += cfg.Matrix[1] * (_state.BgMosaicSizeY + 1);
|
|
cfg.LatchOriginY += cfg.Matrix[3] * (_state.BgMosaicSizeY + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
//TODOGBA behavior for shape == 3? current behaves like shape == 0
|
|
static constexpr uint8_t _sprSize[4][4][2] = {
|
|
{ { 8, 8 }, { 16, 8 }, { 8, 16 }, { 8, 8 } },
|
|
{ { 16, 16 }, { 32, 8 }, { 8, 32 }, { 16, 16 } },
|
|
{ { 32, 32 }, { 32, 16 }, { 16, 32 }, { 32, 32 } },
|
|
{ { 64, 64 }, { 64, 32 }, { 32, 64 }, { 64, 64 } }
|
|
};
|
|
|
|
void GbaPpu::ProcessSprites()
|
|
{
|
|
if(!_emu->GetSettings()->GetGbaConfig().DisableSprites && _state.ObjLayerEnabled && (_state.Scanline <= 159 || _state.Scanline == 227)) {
|
|
if(_state.BgMode >= 3) {
|
|
RenderSprites<true>();
|
|
} else {
|
|
RenderSprites<false>();
|
|
}
|
|
}
|
|
}
|
|
|
|
void GbaPpu::InitSpriteEvaluation()
|
|
{
|
|
_oamScanline = _state.Scanline == 227 ? 0 : (_state.Scanline + 1);
|
|
if(_oamScanline == 0) {
|
|
_oamMosaicY = 0;
|
|
} else {
|
|
if(_oamMosaicY == _state.ObjMosaicSizeY) {
|
|
_oamMosaicY = 0;
|
|
} else {
|
|
_oamMosaicY = (_oamMosaicY + 1) & 0x0F;
|
|
}
|
|
}
|
|
_loadOamAttr01 = true;
|
|
_loadOamAttr2 = false;
|
|
_isFirstOamTileLoad = false;
|
|
_loadOamTileCounter = 0;
|
|
_loadObjMatrix = 0;
|
|
_evalOamIndex = -1;
|
|
|
|
//Update window data
|
|
if(_oamHasWindowModeSprite) {
|
|
for(int x = 0; x < 240; x++) {
|
|
if(_oamWindow[x] == GbaPpu::ObjWindow && _activeWindow[x] == GbaPpu::OutsideWindow) {
|
|
_activeWindow[x] = _oamWindow[x];
|
|
}
|
|
}
|
|
}
|
|
|
|
_oamHasWindowModeSprite = false;
|
|
|
|
std::swap(_oamWriteOutput, _oamReadOutput);
|
|
std::fill(_oamWriteOutput, _oamWriteOutput + 240, GbaPixelData {});
|
|
memset(_oamWindow, GbaPpu::BackdropLayerIndex, sizeof(_oamWindow));
|
|
|
|
if(_state.ObjEnableTimer) {
|
|
_state.ObjEnableTimer--;
|
|
}
|
|
}
|
|
|
|
void GbaPpu::AddVisibleSprite(uint32_t sprData)
|
|
{
|
|
GbaSpriteRendererData& spr = _objData[0];
|
|
|
|
spr.Width = _sprSize[spr.Size][spr.Shape][0];
|
|
spr.Mosaic = sprData & 0x1000;
|
|
if(spr.Mosaic) {
|
|
spr.YOffset = std::max(0, (int)spr.YOffset - (int)_oamMosaicY);
|
|
}
|
|
|
|
spr.Bpp8Mode = sprData & 0x2000;
|
|
spr.SpriteX = (sprData >> 16) & 0x1FF;
|
|
spr.HoriMirror = sprData & 0x10000000;
|
|
spr.VertMirror = sprData & 0x20000000;
|
|
spr.TransformParamSelect = (sprData >> 25) & 0x1F;
|
|
spr.RenderX = 0;
|
|
|
|
_loadOamAttr01 = false;
|
|
_loadOamAttr2 = true;
|
|
|
|
if(spr.SpriteX >= 240) {
|
|
//Skip hidden pixels that wrap around to the right side of the coordinate space
|
|
int gap = -((int)spr.SpriteX - 512);
|
|
spr.RenderX += spr.TransformEnabled ? gap : (gap & ~0x01);
|
|
if(spr.RenderX >= (spr.Width << (uint8_t)spr.DoubleSize)) {
|
|
//Skip this sprite entirely (it's completely hidden)
|
|
_loadOamAttr01 = true;
|
|
_loadOamAttr2 = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GbaPpu::LoadSpriteAttr2()
|
|
{
|
|
GbaSpriteRendererData& spr = _objData[0];
|
|
uint32_t sprData = _oam[spr.Addr + 1];
|
|
spr.TileIndex = sprData & 0x3FF;
|
|
spr.Priority = (sprData >> 10) & 0x03;
|
|
spr.PaletteIndex = spr.Bpp8Mode ? 0 : ((sprData >> 12) & 0x0F);
|
|
|
|
_loadOamAttr2 = false;
|
|
if(spr.TransformEnabled) {
|
|
_loadObjMatrix = 4;
|
|
} else {
|
|
_loadOamAttr01 = true;
|
|
_isFirstOamTileLoad = true;
|
|
_loadOamTileCounter = (spr.Width - spr.RenderX) / 2;
|
|
_objData[1] = spr;
|
|
}
|
|
}
|
|
|
|
void GbaPpu::LoadSpriteTransformMatrix()
|
|
{
|
|
GbaSpriteRendererData& spr = _objData[0];
|
|
_loadObjMatrix--;
|
|
|
|
uint8_t i = 3 - _loadObjMatrix;
|
|
uint16_t transformAddr = (spr.TransformParamSelect << 3) + 1 + (i * 2);
|
|
spr.MatrixData[i] = _oam[transformAddr] >> 16;
|
|
|
|
if(_loadObjMatrix == 0) {
|
|
spr.CenterX = spr.Width / 2;
|
|
spr.CenterY = spr.Height / 2;
|
|
|
|
int32_t originX = -(spr.CenterX << (uint8_t)spr.DoubleSize);
|
|
int32_t originY = -(spr.CenterY << (uint8_t)spr.DoubleSize);
|
|
|
|
spr.XValue = originX * spr.MatrixData[0] + (originY + spr.YOffset) * spr.MatrixData[1];
|
|
spr.YValue = originX * spr.MatrixData[2] + (originY + spr.YOffset) * spr.MatrixData[3];
|
|
spr.XValue += spr.MatrixData[0] * spr.RenderX;
|
|
spr.YValue += spr.MatrixData[2] * spr.RenderX;
|
|
_loadOamTileCounter = (spr.Width << (uint8_t)spr.DoubleSize) - spr.RenderX;
|
|
_loadOamAttr01 = true;
|
|
_isFirstOamTileLoad = true;
|
|
_objData[1] = spr;
|
|
}
|
|
}
|
|
|
|
template<bool blockFirst16k>
|
|
void GbaPpu::RenderSprites()
|
|
{
|
|
if(_oamScanline >= 160) {
|
|
return;
|
|
}
|
|
|
|
uint16_t ppuCycle = _state.Cycle >= 308 * 4 ? (308 * 4) - 1 : _state.Cycle;
|
|
if(_state.AllowHblankOamAccess && ppuCycle > 996) {
|
|
//Evaluation stops just before hblank when this is enabled
|
|
ppuCycle = 996;
|
|
}
|
|
int cycle = _oamLastCycle + 1;
|
|
|
|
if(cycle < 40 && (_state.AllowHblankOamAccess || _evalOamIndex >= 128 || _state.ObjEnableTimer > 0)) {
|
|
//Evaluation in hblank is disabled, jump to cycle 40 (eval start)
|
|
cycle = 40;
|
|
} else if(cycle >= 40 && _state.ObjEnableTimer > 0) {
|
|
return;
|
|
}
|
|
|
|
if(cycle & 0x01) {
|
|
cycle++;
|
|
}
|
|
GbaSpriteRendererData& spr = _objData[0];
|
|
for(; cycle <= ppuCycle; cycle+=2) {
|
|
if(cycle == 40) {
|
|
//start oam evaluation/fetching
|
|
InitSpriteEvaluation();
|
|
if(_oamScanline == 160 || _state.ObjEnableTimer > 0) {
|
|
//Scanline 159 stops processing sprites after cycle 39
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool allowLoadAttr01 = _loadOamTileCounter <= 1 || _isFirstOamTileLoad;
|
|
|
|
if(_loadOamTileCounter) {
|
|
if(_isFirstOamTileLoad && _objData[1].TransformEnabled) {
|
|
_isFirstOamTileLoad = false;
|
|
} else {
|
|
//load+draw pixels
|
|
_isFirstOamTileLoad = false;
|
|
_loadOamTileCounter--;
|
|
_memoryAccess[cycle] |= GbaPpuMemAccess::VramObj;
|
|
|
|
//Last cycle (39) doesn't actually draw, but cycle 38 does read from VRAM anyway
|
|
if(cycle != 38) {
|
|
if(_objData[1].TransformEnabled) {
|
|
RenderSprite<true, blockFirst16k>(_objData[1]);
|
|
} else {
|
|
RenderSprite<false, blockFirst16k>(_objData[1]);
|
|
}
|
|
}
|
|
|
|
if(_evalOamIndex >= 128 && _loadOamTileCounter == 0) {
|
|
//Finished loading last sprite
|
|
if(cycle < 40) {
|
|
//Jump to the start of the next evaluation cycle (cycle 40)
|
|
cycle = 38;
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(_loadOamAttr01 && allowLoadAttr01) {
|
|
//Load first 4 bytes of OAM attributes, evaluate if sprite should appear on this scanline
|
|
_evalOamIndex++;
|
|
if(_evalOamIndex >= 128) {
|
|
if(_loadOamTileCounter == 0) {
|
|
//Finished loading last sprite
|
|
if(cycle < 40) {
|
|
//Jump to the start of the next evaluation cycle (cycle 40)
|
|
cycle = 38;
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
_loadOamAttr01 = false;
|
|
continue;
|
|
}
|
|
|
|
_memoryAccess[cycle] |= GbaPpuMemAccess::Oam;
|
|
|
|
uint16_t addr = _evalOamIndex << 1;
|
|
uint32_t sprData = _oam[addr];
|
|
spr.TransformEnabled = sprData & 0x0100;
|
|
spr.DoubleSize = sprData & 0x0200;
|
|
spr.HideSprite = !spr.TransformEnabled && spr.DoubleSize;
|
|
spr.Mode = (GbaPpuObjMode)((sprData >> 10) & 0x03);
|
|
if(!spr.HideSprite && spr.Mode != GbaPpuObjMode::Invalid) {
|
|
spr.SpriteY = sprData & 0xFF;
|
|
spr.Shape = (sprData >> 14) & 0x03;
|
|
spr.Size = (sprData >> 30) & 0x03;
|
|
spr.Height = _sprSize[spr.Size][spr.Shape][1];
|
|
spr.YOffset = _oamScanline - spr.SpriteY;
|
|
|
|
uint8_t sprHeight = (spr.Height << (uint8_t)spr.DoubleSize);
|
|
if(spr.YOffset < sprHeight && _oamScanline < (uint8_t)(spr.SpriteY + sprHeight)) {
|
|
//sprite is visible on this scanline
|
|
spr.Addr = addr;
|
|
AddVisibleSprite(sprData);
|
|
}
|
|
}
|
|
} else if(_loadOamAttr2 && _loadOamTileCounter == 0) {
|
|
//Load last 2 bytes of OAM attributes
|
|
_memoryAccess[cycle] |= GbaPpuMemAccess::Oam;
|
|
LoadSpriteAttr2();
|
|
} else if(_loadObjMatrix) {
|
|
//Load all 4 16-bit transform parameters (when transform flag is enabled)
|
|
_memoryAccess[cycle] |= GbaPpuMemAccess::Oam;
|
|
LoadSpriteTransformMatrix();
|
|
}
|
|
}
|
|
|
|
_oamLastCycle = _state.Cycle;
|
|
}
|
|
|
|
template<bool blockFirst16k>
|
|
uint8_t GbaPpu::ReadSpriteVram(uint32_t addr)
|
|
{
|
|
if constexpr(blockFirst16k) {
|
|
//When in BG modes 3-5, the first 16kb of sprite tile vram ($10000-$13FFF) can't be accessed
|
|
//by sprites (because the BG layer uses it), reading it returns 0
|
|
return addr < 0x4000 ? 0 : _vram[0x10000 | (addr & 0x7FFF)];
|
|
} else {
|
|
return _vram[0x10000 | (addr & 0x7FFF)];
|
|
}
|
|
}
|
|
|
|
template<bool transformEnabled, bool blockFirst16k>
|
|
void GbaPpu::RenderSprite(GbaSpriteRendererData& spr)
|
|
{
|
|
for(int i = 0; i < (transformEnabled ? 1 : 2); i++) {
|
|
if constexpr(transformEnabled) {
|
|
spr.XPos = (spr.XValue >> 8) + spr.CenterX;
|
|
spr.YPos = (spr.YValue >> 8) + spr.CenterY;
|
|
spr.XValue += spr.MatrixData[0];
|
|
spr.YValue += spr.MatrixData[2];
|
|
} else {
|
|
spr.XPos = spr.HoriMirror ? (spr.Width - spr.RenderX - 1) : spr.RenderX;
|
|
spr.YPos = spr.VertMirror ? (spr.Height - spr.YOffset - 1) : spr.YOffset;
|
|
}
|
|
|
|
uint16_t drawPos = (spr.SpriteX + spr.RenderX) & 0x1FF;
|
|
spr.RenderX++;
|
|
|
|
if(drawPos >= 240 || spr.XPos >= spr.Width || spr.YPos >= spr.Height) {
|
|
continue;
|
|
}
|
|
|
|
bool isHigherPriority = _oamWriteOutput[drawPos].Priority > spr.Priority;
|
|
if(spr.Mode == GbaPpuObjMode::Window || isHigherPriority) {
|
|
uint8_t tileRow = spr.YPos / 8;
|
|
uint8_t tileColumn = spr.XPos / 8;
|
|
uint16_t index;
|
|
if(_state.ObjVramMappingOneDimension) {
|
|
uint8_t tilesPerRow = (spr.Width / 8) * (spr.Bpp8Mode ? 2 : 1);
|
|
index = _objData[0].TileIndex + tileRow * tilesPerRow + (spr.Bpp8Mode ? tileColumn * 2 : tileColumn);
|
|
} else {
|
|
index = (_objData[0].TileIndex & ~0x1F) + tileRow * 0x20 + (((_objData[0].TileIndex & 0x1F) + (spr.Bpp8Mode ? tileColumn * 2 : tileColumn)) & 0x1F);
|
|
}
|
|
uint8_t color = 0;
|
|
|
|
if(spr.Bpp8Mode) {
|
|
color = ReadSpriteVram<blockFirst16k>(index * 32 + (spr.YPos & 0x07) * 8 + (spr.XPos & 0x07));
|
|
} else {
|
|
color = ReadSpriteVram<blockFirst16k>(index * 32 + (spr.YPos & 0x07) * 4 + ((spr.XPos & 0x07) >> 1));
|
|
if(spr.XPos & 0x01) {
|
|
color >>= 4;
|
|
} else {
|
|
color &= 0x0F;
|
|
}
|
|
}
|
|
|
|
if(color != 0) {
|
|
if(spr.Mode == GbaPpuObjMode::Window) {
|
|
_oamHasWindowModeSprite = true;
|
|
_oamWindow[drawPos] = GbaPpu::ObjWindow;
|
|
} else {
|
|
uint16_t colorIndex = 0x200 + (spr.PaletteIndex * 32) + color * 2;
|
|
if(spr.Mode == GbaPpuObjMode::Blending) {
|
|
colorIndex |= GbaPpu::SpriteBlendFlag;
|
|
}
|
|
SetPixelData(_oamWriteOutput[drawPos], colorIndex, spr.Priority, GbaPpu::SpriteLayerIndex);
|
|
}
|
|
} else if(isHigherPriority && _oamWriteOutput[drawPos].Priority != 0xFF) {
|
|
//If a sprite pixel already exists and another sprite with higher priority with a
|
|
//transparent pixel is displayed here, the priority is updated to match the transparent
|
|
//pixel's priority (Golden Sun requires this)
|
|
_oamWriteOutput[drawPos].Priority = spr.Priority;
|
|
}
|
|
|
|
if(spr.Mosaic) {
|
|
_oamWriteOutput[drawPos].Color |= GbaPpu::SpriteMosaicFlag;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void GbaPpu::SetWindowActiveLayers(int window, uint8_t cfg)
|
|
{
|
|
for(int i = 0; i < 6; i++) {
|
|
_state.WindowActiveLayers[window][i] = cfg & (1 << i);
|
|
}
|
|
}
|
|
|
|
template<int bit>
|
|
void GbaPpu::SetTransformOrigin(uint8_t i, uint8_t value, bool setY)
|
|
{
|
|
if(setY) {
|
|
BitUtilities::SetBits<bit>(_state.Transform[i].OriginY, value);
|
|
_state.Transform[i].PendingUpdateY = true;
|
|
} else {
|
|
BitUtilities::SetBits<bit>(_state.Transform[i].OriginX, value);
|
|
_state.Transform[i].PendingUpdateX = true;
|
|
}
|
|
}
|
|
|
|
void GbaPpu::SetLayerEnabled(int layer, bool enabled)
|
|
{
|
|
if(_state.BgLayers[layer].Enabled || !enabled) {
|
|
_state.BgLayers[layer].Enabled = enabled;
|
|
_state.BgLayers[layer].EnableTimer = 0;
|
|
} else if(_state.BgLayers[layer].EnableTimer == 0) {
|
|
_state.BgLayers[layer].EnableTimer = 2;
|
|
}
|
|
}
|
|
|
|
void GbaPpu::WriteRegister(uint32_t addr, uint8_t value)
|
|
{
|
|
if(_lastRenderCycle != _state.Cycle && (_state.Scanline < 160 || _state.Scanline == 227)) {
|
|
if(_state.Cycle < 1006 || addr <= 0x01 || addr == 0x4D || addr >= 0x40 && addr <= 0x43) {
|
|
//Only run renderer during active rendering (< 1006), or if the write could affect sprites/window processing
|
|
RenderScanline(true);
|
|
}
|
|
}
|
|
|
|
switch(addr) {
|
|
case 0x00:
|
|
_state.Control = value & ~0x08;
|
|
_state.BgMode = value & 0x07;
|
|
_state.DisplayFrameSelect = value & 0x10;
|
|
_state.AllowHblankOamAccess = value & 0x20;
|
|
_state.ObjVramMappingOneDimension = value & 0x40;
|
|
_state.ForcedBlank = value & 0x80;
|
|
break;
|
|
|
|
case 0x01: {
|
|
_state.Control2 = value;
|
|
SetLayerEnabled(0, value & 0x01);
|
|
SetLayerEnabled(1, value & 0x02);
|
|
SetLayerEnabled(2, value & 0x04);
|
|
SetLayerEnabled(3, value & 0x08);
|
|
bool objEnabled = value & 0x10;
|
|
if(_state.ObjLayerEnabled && !objEnabled) {
|
|
//Clear sprite row buffers when OBJ layer is disabled
|
|
//TODOGBA what does hardware do if sprites are re-enabled on the same or next scanline?
|
|
std::fill(_oamOutputBuffers[0], _oamOutputBuffers[0] + 240, GbaPixelData {});
|
|
std::fill(_oamOutputBuffers[1], _oamOutputBuffers[1] + 240, GbaPixelData {});
|
|
} else if(!_state.ObjLayerEnabled && objEnabled) {
|
|
_state.ObjEnableTimer = _state.Scanline >= 160 ? 0 : 3;
|
|
}
|
|
_state.ObjLayerEnabled = objEnabled;
|
|
_state.Window0Enabled = value & 0x20;
|
|
_state.Window1Enabled = value & 0x40;
|
|
_state.ObjWindowEnabled = value & 0x80;
|
|
break;
|
|
}
|
|
|
|
case 0x02: _state.GreenSwapEnabled = value & 0x01; break;
|
|
case 0x03: break;
|
|
|
|
case 0x04:
|
|
_state.DispStat = value & 0x38;
|
|
_state.VblankIrqEnabled = value & 0x08;
|
|
_state.HblankIrqEnabled = value & 0x10;
|
|
_state.ScanlineIrqEnabled = value & 0x20;
|
|
break;
|
|
|
|
case 0x05:
|
|
if(_state.Lyc != value) {
|
|
_state.Lyc = value;
|
|
|
|
//If LYC is changed to the current scanline and LYC IRQs are enabled, trigger it
|
|
//(writing when LYC is already set to the current scanline doesn't trigger an irq - doing this breaks audio in Minish Cap)
|
|
if(_state.ScanlineIrqEnabled && _state.Scanline == _state.Lyc) {
|
|
_console->GetMemoryManager()->SetIrqSource(GbaIrqSource::LcdScanlineMatch);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 0x08: case 0x0A: case 0x0C: case 0x0E: {
|
|
GbaBgConfig& cfg = _state.BgLayers[(addr & 0x06) >> 1];
|
|
BitUtilities::SetBits<0>(cfg.Control, value);
|
|
cfg.Priority = value & 0x03;
|
|
cfg.TilesetAddr = ((value >> 2) & 0x03) * 0x4000;
|
|
//Bits 4-5 unused
|
|
cfg.Mosaic = value & 0x40;
|
|
cfg.Bpp8Mode = value & 0x80;
|
|
break;
|
|
}
|
|
|
|
case 0x09: case 0x0B: case 0x0D: case 0x0F: {
|
|
uint8_t layer = (addr & 0x06) >> 1;
|
|
GbaBgConfig& cfg = _state.BgLayers[layer];
|
|
if(layer < 2) {
|
|
//unused bit
|
|
value &= ~0x20;
|
|
}
|
|
|
|
BitUtilities::SetBits<8>(cfg.Control, value);
|
|
cfg.TilemapAddr = (value & 0x1F) * 0x800;
|
|
cfg.WrapAround = value & 0x20;
|
|
cfg.DoubleWidth = value & 0x40;
|
|
cfg.DoubleHeight = value & 0x80;
|
|
cfg.ScreenSize = (value >> 6) & 0x03;
|
|
break;
|
|
}
|
|
|
|
case 0x10: case 0x14: case 0x18: case 0x1C:
|
|
BitUtilities::SetBits<0>(_state.BgLayers[(addr & 0x0C) >> 2].ScrollX, value);
|
|
break;
|
|
|
|
case 0x11: case 0x15: case 0x19: case 0x1D:
|
|
BitUtilities::SetBits<8>(_state.BgLayers[(addr & 0x0C) >> 2].ScrollX, value);
|
|
break;
|
|
|
|
case 0x12: case 0x16: case 0x1A: case 0x1E:
|
|
BitUtilities::SetBits<0>(_state.BgLayers[(addr & 0x0C) >> 2].ScrollY, value);
|
|
break;
|
|
|
|
case 0x13: case 0x17: case 0x1B: case 0x1F:
|
|
BitUtilities::SetBits<8>(_state.BgLayers[(addr & 0x0C) >> 2].ScrollY, value);
|
|
break;
|
|
|
|
case 0x20: case 0x22: case 0x24: case 0x26:
|
|
case 0x30: case 0x32: case 0x34: case 0x36:
|
|
BitUtilities::SetBits<0>(_state.Transform[(addr & 0x10) >> 4].Matrix[(addr & 0x06) >> 1], value);
|
|
break;
|
|
|
|
case 0x21: case 0x23: case 0x25: case 0x27:
|
|
case 0x31: case 0x33: case 0x35: case 0x37:
|
|
BitUtilities::SetBits<8>(_state.Transform[(addr & 0x10) >> 4].Matrix[(addr & 0x06) >> 1], value);
|
|
break;
|
|
|
|
case 0x28: case 0x38: SetTransformOrigin<0>((addr & 0x10) >> 4, value, false); break;
|
|
case 0x29: case 0x39: SetTransformOrigin<8>((addr & 0x10) >> 4, value, false); break;
|
|
case 0x2A: case 0x3A: SetTransformOrigin<16>((addr & 0x10) >> 4, value, false); break;
|
|
case 0x2B: case 0x3B: SetTransformOrigin<24>((addr & 0x10) >> 4, value & 0x0F, false); break;
|
|
|
|
case 0x2C: case 0x3C: SetTransformOrigin<0>((addr & 0x10) >> 4, value, true); break;
|
|
case 0x2D: case 0x3D: SetTransformOrigin<8>((addr & 0x10) >> 4, value, true); break;
|
|
case 0x2E: case 0x3E: SetTransformOrigin<16>((addr & 0x10) >> 4, value, true); break;
|
|
case 0x2F: case 0x3F: SetTransformOrigin<24>((addr & 0x10) >> 4, value & 0x0F, true); break;
|
|
|
|
case 0x40: SetWindowX(_state.Window[0].RightX, value); break;
|
|
case 0x41: SetWindowX(_state.Window[0].LeftX, value); break;
|
|
case 0x42: SetWindowX(_state.Window[1].RightX, value); break;
|
|
case 0x43: SetWindowX(_state.Window[1].LeftX, value); break;
|
|
|
|
case 0x44: _state.Window[0].BottomY = value; break;
|
|
case 0x45: _state.Window[0].TopY = value; break;
|
|
case 0x46: _state.Window[1].BottomY = value; break;
|
|
case 0x47: _state.Window[1].TopY = value; break;
|
|
|
|
case 0x48: _state.Window0Control = value & 0x3F; SetWindowActiveLayers(0, value & 0x3F); break;
|
|
case 0x49: _state.Window1Control = value & 0x3F; SetWindowActiveLayers(1, value & 0x3F); break;
|
|
case 0x4A: _state.OutWindowControl = value & 0x3F; SetWindowActiveLayers(3, value & 0x3F); break;
|
|
case 0x4B: _state.ObjWindowControl = value & 0x3F; SetWindowActiveLayers(2, value & 0x3F); break;
|
|
|
|
case 0x4C:
|
|
_state.BgMosaicSizeX = value & 0x0F;
|
|
_state.BgMosaicSizeY = (value >> 4) & 0x0F;
|
|
break;
|
|
|
|
case 0x4D:
|
|
_state.ObjMosaicSizeX = value & 0x0F;
|
|
_state.ObjMosaicSizeY = (value >> 4) & 0x0F;
|
|
break;
|
|
|
|
case 0x50:
|
|
_state.BlendMainControl = value;
|
|
_state.BlendMain[0] = value & 0x01; //BG0
|
|
_state.BlendMain[1] = value & 0x02; //BG1
|
|
_state.BlendMain[2] = value & 0x04; //BG2
|
|
_state.BlendMain[3] = value & 0x08; //BG3
|
|
_state.BlendMain[GbaPpu::SpriteLayerIndex] = value & 0x10;
|
|
_state.BlendMain[GbaPpu::BackdropLayerIndex] = value & 0x20;
|
|
_state.BlendEffect = (GbaPpuBlendEffect)((value >> 6) & 0x03);
|
|
break;
|
|
|
|
case 0x51:
|
|
_state.BlendSubControl = value & 0x3F;
|
|
_state.BlendSub[0] = value & 0x01;
|
|
_state.BlendSub[1] = value & 0x02;
|
|
_state.BlendSub[2] = value & 0x04;
|
|
_state.BlendSub[3] = value & 0x08;
|
|
_state.BlendSub[GbaPpu::SpriteLayerIndex] = value & 0x10;
|
|
_state.BlendSub[GbaPpu::BackdropLayerIndex] = value & 0x20;
|
|
break;
|
|
|
|
case 0x52: _state.BlendMainCoefficient = value & 0x1F; break;
|
|
case 0x53: _state.BlendSubCoefficient = value & 0x1F; break;
|
|
case 0x54: _state.Brightness = value & 0x1F; break;
|
|
|
|
default:
|
|
LogDebug("Write unimplemented LCD register: " + HexUtilities::ToHex32(addr) + " = " + HexUtilities::ToHex(value));
|
|
break;
|
|
}
|
|
}
|
|
|
|
uint8_t GbaPpu::ReadRegister(uint32_t addr)
|
|
{
|
|
switch(addr) {
|
|
case 0x00: return _state.Control;
|
|
case 0x01: return _state.Control2;
|
|
|
|
case 0x02: return (uint8_t)_state.GreenSwapEnabled;
|
|
case 0x03: return 0;
|
|
|
|
case 0x04:
|
|
return (
|
|
(_state.Scanline >= 160 && _state.Scanline != 227 ? 0x01 : 0) |
|
|
((_state.Cycle > 1007 || _state.Cycle == 0) ? 0x02 : 0) |
|
|
(_state.Scanline == _state.Lyc ? 0x04 : 0) |
|
|
_state.DispStat
|
|
);
|
|
|
|
case 0x05: return _state.Lyc;
|
|
|
|
case 0x06: return _state.Scanline;
|
|
case 0x07: return 0;
|
|
|
|
case 0x08: case 0x0A: case 0x0C: case 0x0E:
|
|
return (uint8_t)_state.BgLayers[(addr & 0x06) >> 1].Control;
|
|
|
|
case 0x09: case 0x0B: case 0x0D: case 0x0F:
|
|
return (uint8_t)(_state.BgLayers[(addr & 0x06) >> 1].Control >> 8);
|
|
|
|
case 0x48: return _state.Window0Control;
|
|
case 0x49: return _state.Window1Control;
|
|
case 0x4A: return _state.OutWindowControl;
|
|
case 0x4B: return _state.ObjWindowControl;
|
|
|
|
case 0x50: return _state.BlendMainControl;
|
|
case 0x51: return _state.BlendSubControl;
|
|
case 0x52: return _state.BlendMainCoefficient;
|
|
case 0x53: return _state.BlendSubCoefficient;
|
|
|
|
default:
|
|
LogDebug("Read unimplemented LCD register: " + HexUtilities::ToHex32(addr));
|
|
break;
|
|
}
|
|
|
|
return _memoryManager->GetOpenBus(addr);
|
|
}
|
|
|
|
void GbaPpu::DebugProcessMemoryAccessView()
|
|
{
|
|
//Store memory access buffer in ppu tools to display in tilemap viewer
|
|
GbaPpuTools* ppuTools = ((GbaPpuTools*)_emu->InternalGetDebugger()->GetPpuTools(CpuType::Gba));
|
|
ppuTools->SetMemoryAccessData(_state.Scanline, _memoryAccess);
|
|
}
|
|
|
|
void GbaPpu::Serialize(Serializer& s)
|
|
{
|
|
SV(_state.FrameCount);
|
|
SV(_state.Cycle);
|
|
SV(_state.Scanline);
|
|
|
|
SV(_state.Control);
|
|
SV(_state.BgMode);
|
|
SV(_state.DisplayFrameSelect);
|
|
SV(_state.AllowHblankOamAccess);
|
|
SV(_state.ObjVramMappingOneDimension);
|
|
SV(_state.ForcedBlank);
|
|
SV(_state.GreenSwapEnabled);
|
|
|
|
SV(_state.Control2);
|
|
SV(_state.ObjEnableTimer);
|
|
SV(_state.ObjLayerEnabled);
|
|
SV(_state.Window0Enabled);
|
|
SV(_state.Window1Enabled);
|
|
SV(_state.ObjWindowEnabled);
|
|
|
|
SV(_state.DispStat);
|
|
SV(_state.VblankIrqEnabled);
|
|
SV(_state.HblankIrqEnabled);
|
|
SV(_state.ScanlineIrqEnabled);
|
|
SV(_state.Lyc);
|
|
|
|
SV(_state.BlendMainControl);
|
|
SVArray(_state.BlendMain, 6);
|
|
SV(_state.BlendSubControl);
|
|
SVArray(_state.BlendSub, 6);
|
|
SV(_state.BlendEffect);
|
|
SV(_state.BlendMainCoefficient);
|
|
SV(_state.BlendSubCoefficient);
|
|
SV(_state.Brightness);
|
|
|
|
for(int i = 0; i < 4; i++) {
|
|
SVI(_state.BgLayers[i].Control);
|
|
SVI(_state.BgLayers[i].TilemapAddr);
|
|
SVI(_state.BgLayers[i].TilesetAddr);
|
|
SVI(_state.BgLayers[i].ScrollX);
|
|
SVI(_state.BgLayers[i].ScrollXLatch);
|
|
SVI(_state.BgLayers[i].ScrollY);
|
|
SVI(_state.BgLayers[i].ScreenSize);
|
|
SVI(_state.BgLayers[i].DoubleWidth);
|
|
SVI(_state.BgLayers[i].DoubleHeight);
|
|
SVI(_state.BgLayers[i].Priority);
|
|
SVI(_state.BgLayers[i].Mosaic);
|
|
SVI(_state.BgLayers[i].WrapAround);
|
|
SVI(_state.BgLayers[i].Bpp8Mode);
|
|
SVI(_state.BgLayers[i].Enabled);
|
|
SVI(_state.BgLayers[i].EnableTimer);
|
|
}
|
|
|
|
for(int i = 0; i < 2; i++) {
|
|
SVI(_state.Transform[i].OriginX);
|
|
SVI(_state.Transform[i].OriginY);
|
|
SVI(_state.Transform[i].LatchOriginX);
|
|
SVI(_state.Transform[i].LatchOriginY);
|
|
SVI(_state.Transform[i].PendingUpdateX);
|
|
SVI(_state.Transform[i].PendingUpdateY);
|
|
SVI(_state.Transform[i].Matrix[0]);
|
|
SVI(_state.Transform[i].Matrix[1]);
|
|
SVI(_state.Transform[i].Matrix[2]);
|
|
SVI(_state.Transform[i].Matrix[3]);
|
|
|
|
SVI(_state.Window[i].LeftX);
|
|
SVI(_state.Window[i].RightX);
|
|
SVI(_state.Window[i].TopY);
|
|
SVI(_state.Window[i].BottomY);
|
|
}
|
|
|
|
SV(_state.BgMosaicSizeX);
|
|
SV(_state.BgMosaicSizeY);
|
|
SV(_state.ObjMosaicSizeX);
|
|
SV(_state.ObjMosaicSizeY);
|
|
|
|
SV(_state.Window0Control);
|
|
SV(_state.Window1Control);
|
|
SV(_state.ObjWindowControl);
|
|
SV(_state.OutWindowControl);
|
|
|
|
if(s.GetFormat() != SerializeFormat::Map) {
|
|
for(int i = 0; i < 5; i++) {
|
|
SVI(_state.WindowActiveLayers[i][0]);
|
|
SVI(_state.WindowActiveLayers[i][1]);
|
|
SVI(_state.WindowActiveLayers[i][2]);
|
|
SVI(_state.WindowActiveLayers[i][3]);
|
|
SVI(_state.WindowActiveLayers[i][4]);
|
|
SVI(_state.WindowActiveLayers[i][5]);
|
|
}
|
|
|
|
//Convert data to plain arrays to improve serialization performance
|
|
GbaPixelData* src[6] = { _oamOutputBuffers[0], _oamOutputBuffers[1], _layerOutput[0], _layerOutput[1], _layerOutput[2], _layerOutput[3] };
|
|
string names[6] = { "oamOutputBuffers[0]", "oamOutputBuffers[1]", "layerOutput[0]", "layerOutput[1]", "layerOutput[2]", "layerOutput[3]" };
|
|
for(int i = 0; i < 6; i++) {
|
|
GbaPixelData* data = src[i];
|
|
uint16_t color[GbaConstants::ScreenWidth];
|
|
uint8_t layer[GbaConstants::ScreenWidth];
|
|
uint8_t priority[GbaConstants::ScreenWidth];
|
|
if(s.IsSaving()) {
|
|
for(int j = 0; j < GbaConstants::ScreenWidth; j++) {
|
|
color[j] = data[j].Color;
|
|
layer[j] = data[j].Layer;
|
|
priority[j] = data[j].Priority;
|
|
}
|
|
}
|
|
s.StreamArray(color, GbaConstants::ScreenWidth, (names[i] + "_color").c_str());
|
|
s.StreamArray(layer, GbaConstants::ScreenWidth, (names[i] + "_layer").c_str());
|
|
s.StreamArray(priority, GbaConstants::ScreenWidth, (names[i] + "_priority").c_str());
|
|
if(!s.IsSaving()) {
|
|
for(int j = 0; j < GbaConstants::ScreenWidth; j++) {
|
|
data[j].Color = color[j];
|
|
data[j].Layer = layer[j];
|
|
data[j].Priority = priority[j];
|
|
}
|
|
}
|
|
}
|
|
|
|
SVArray(_oamWindow, GbaConstants::ScreenWidth);
|
|
SVArray(_activeWindow, GbaConstants::ScreenWidth);
|
|
SVArray(_memoryAccess, 308 * 4);
|
|
|
|
SV(_triggerSpecialDma);
|
|
|
|
SV(_lastWindowCycle);
|
|
SV(_window0ActiveY);
|
|
SV(_window1ActiveY);
|
|
SV(_window0ActiveX);
|
|
SV(_window1ActiveX);
|
|
|
|
SV(_lastRenderCycle);
|
|
SV(_evalOamIndex);
|
|
SV(_loadOamTileCounter);
|
|
SV(_oamHasWindowModeSprite);
|
|
SV(_loadOamAttr01);
|
|
SV(_loadOamAttr2);
|
|
SV(_isFirstOamTileLoad);
|
|
SV(_loadObjMatrix);
|
|
SV(_oamScanline);
|
|
SV(_oamMosaicY);
|
|
|
|
for(int i = 0; i < 4; i++) {
|
|
SVI(_layerData[i].TilemapData);
|
|
SVI(_layerData[i].TileData);
|
|
SVI(_layerData[i].FetchAddr);
|
|
SVI(_layerData[i].TileIndex);
|
|
SVI(_layerData[i].RenderX);
|
|
SVI(_layerData[i].TransformX);
|
|
SVI(_layerData[i].TransformY);
|
|
SVI(_layerData[i].XPos);
|
|
SVI(_layerData[i].YPos);
|
|
SVI(_layerData[i].PaletteIndex);
|
|
SVI(_layerData[i].HoriMirror);
|
|
SVI(_layerData[i].VertMirror);
|
|
SVI(_layerData[i].TileRow);
|
|
SVI(_layerData[i].TileColumn);
|
|
SVI(_layerData[i].MosaicColor);
|
|
}
|
|
|
|
for(int i = 0; i < 2; i++) {
|
|
SVI(_objData[i].MatrixData[0]);
|
|
SVI(_objData[i].MatrixData[1]);
|
|
SVI(_objData[i].MatrixData[2]);
|
|
SVI(_objData[i].MatrixData[3]);
|
|
SVI(_objData[i].XValue);
|
|
SVI(_objData[i].YValue);
|
|
SVI(_objData[i].XPos);
|
|
SVI(_objData[i].YPos);
|
|
SVI(_objData[i].CenterX);
|
|
SVI(_objData[i].CenterY);
|
|
|
|
SVI(_objData[i].SpriteX);
|
|
SVI(_objData[i].TileIndex);
|
|
SVI(_objData[i].Addr);
|
|
SVI(_objData[i].RenderX);
|
|
SVI(_objData[i].SpriteY);
|
|
SVI(_objData[i].YOffset);
|
|
SVI(_objData[i].TransformEnabled);
|
|
SVI(_objData[i].DoubleSize);
|
|
SVI(_objData[i].HideSprite);
|
|
|
|
SVI(_objData[i].Mode);
|
|
SVI(_objData[i].Mosaic);
|
|
SVI(_objData[i].Bpp8Mode);
|
|
SVI(_objData[i].Shape);
|
|
SVI(_objData[i].HoriMirror);
|
|
SVI(_objData[i].VertMirror);
|
|
SVI(_objData[i].TransformParamSelect);
|
|
SVI(_objData[i].Size);
|
|
|
|
SVI(_objData[i].Priority);
|
|
SVI(_objData[i].PaletteIndex);
|
|
|
|
SVI(_objData[i].Width);
|
|
SVI(_objData[i].Height);
|
|
}
|
|
}
|
|
|
|
if(!s.IsSaving()) {
|
|
UpdateWindowChangePoints();
|
|
}
|
|
}
|