mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
NFL 2K5 (SLUS-20919) hangs at the boot logo in a 64-bit divide-by-repeated- subtraction with a huge unsigned dividend. Its 64-bit clock is an overflow- ISR-maintained wrap accumulator plus a live T0_COUNT read (bus/16, OVFE), reconciled lock-free with double reads — airtight on hardware, where the count wrap and the overflow interrupt are the same edge and the ISR preempts before any later read. Under the JIT the guest can observe the wrap while the ISR's effects are still pending, in two phases: (1) the count (derived from the live cpuRegs.cycle) crosses the boundary before the scheduled rcntUpdate event runs; (2) rcntUpdate has wrapped the count and raised the INTC, but the exception waits for the next event test — which our static-linked / short-block tails defer past the reader's entire load sequence (traced live: the wrap event fires at the reader's own block-entry event test, and delivery lands at its jr-ra exit, 30 cycles too late). Either way the game reads stale-accumulator + wrapped-count, time goes backwards one wrap period, and the divide runs ~2^48 iterations. Clamp the read to just-before-the-boundary until the interrupt has actually been delivered. The deliverability guard (INTC pending & unmasked & Status EIE/IE, no EXL/ERL) makes this exact: inside the handler or with the source masked (e.g. the game's DisableIntc reader, which reconciles the raw wrap itself) the wrapped count stays observable, as on hardware. Pinned by EeTimerCountReadRace.* in recompiler_tests. Verified live: cold fastboot reaches attract; previously parked at the divide loop within ~20s.
1089 lines
36 KiB
C++
1089 lines
36 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include <time.h>
|
|
#include <cmath>
|
|
|
|
#include "Common.h"
|
|
#include "R3000A.h"
|
|
#include "Counters.h"
|
|
#include "IopCounters.h"
|
|
|
|
#include "GS.h"
|
|
#include "GS/GS.h"
|
|
#include "MTGS.h"
|
|
#include "PerformanceMetrics.h"
|
|
#include "Patch.h"
|
|
#include "ps2/HwInternal.h"
|
|
#include "SIO/Sio.h"
|
|
#include "SPU2/spu2.h"
|
|
#include "Recording/InputRecording.h"
|
|
#include "VMManager.h"
|
|
#include "VUmicro.h"
|
|
|
|
static const uint EECNT_FUTURE_TARGET = 0x10000000;
|
|
|
|
uint g_FrameCount = 0;
|
|
|
|
// Counter 4 takes care of scanlines - hSync/hBlanks
|
|
// Counter 5 takes care of vSync/vBlanks
|
|
Counter counters[4];
|
|
SyncCounter hsyncCounter;
|
|
SyncCounter vsyncCounter;
|
|
|
|
u64 nextStartCounter; // records the cpuRegs.cycle value of the last call to rcntUpdate()
|
|
s32 nextDeltaCounter; // delta from nextsCounter, in cycles, until the next rcntUpdate()
|
|
|
|
// Forward declarations needed because C/C++ both are wimpy single-pass compilers.
|
|
|
|
static void rcntStartGate(bool mode, u64 sCycle);
|
|
static void rcntEndGate(bool mode, u64 sCycle);
|
|
static void rcntWcount(int index, u32 value);
|
|
static void rcntWmode(int index, u32 value);
|
|
static void rcntWtarget(int index, u32 value);
|
|
static void rcntWhold(int index, u32 value);
|
|
|
|
// For Analog/Double Strike and Interlace modes
|
|
static bool IsInterlacedVideoMode()
|
|
{
|
|
return (gsVideoMode == GS_VideoMode::PAL || gsVideoMode == GS_VideoMode::NTSC || gsVideoMode == GS_VideoMode::DVD_NTSC || gsVideoMode == GS_VideoMode::DVD_PAL || gsVideoMode == GS_VideoMode::HDTV_1080I);
|
|
}
|
|
|
|
static bool IsProgressiveVideoMode()
|
|
{
|
|
// The FIELD register only flips if the CMOD field in SMODE1 is set to anything but 0 and Front Porch bottom bit in SYNCV is set.
|
|
// Also see "isReallyInterlaced()" in GSState.cpp
|
|
return !(*(u32*)PS2GS_BASE(GS_SYNCV) & 0x1) || !(*(u32*)PS2GS_BASE(GS_SMODE1) & 0x6000);
|
|
}
|
|
|
|
void rcntReset(int index)
|
|
{
|
|
counters[index].count = 0;
|
|
counters[index].startCycle = cpuRegs.cycle;
|
|
}
|
|
|
|
// Updates the state of the nextCounter value (if needed) to serve
|
|
// any pending events for the given counter.
|
|
// Call this method after any modifications to the state of a counter.
|
|
static __fi void _rcntSet(int cntidx)
|
|
{
|
|
s32 c;
|
|
pxAssume(cntidx <= 4); // rcntSet isn't valid for h/vsync counters.
|
|
|
|
const Counter& counter = counters[cntidx];
|
|
|
|
// Stopped or special hsync gate?
|
|
if (!rcntCanCount(cntidx) || (counter.mode.ClockSource == 0x3))
|
|
return;
|
|
|
|
if (!counter.mode.TargetInterrupt && !counter.mode.OverflowInterrupt && !counter.mode.ZeroReturn)
|
|
return;
|
|
// check for special cases where the overflow or target has just passed
|
|
// (we probably missed it because we're doing/checking other things)
|
|
if (counter.count > 0x10000 || counter.count > counter.target)
|
|
{
|
|
nextDeltaCounter = 4;
|
|
return;
|
|
}
|
|
|
|
// nextCounter is relative to the cpuRegs.cycle when rcntUpdate() was last called.
|
|
// However, the current _rcntSet could be called at any cycle count, so we need to take
|
|
// that into account. Adding the difference from that cycle count to the current one
|
|
// will do the trick!
|
|
|
|
c = ((0x10000 - counter.count) * counter.rate) - (cpuRegs.cycle - counter.startCycle);
|
|
c += cpuRegs.cycle - nextStartCounter; // adjust for time passed since last rcntUpdate();
|
|
|
|
if (c < nextDeltaCounter)
|
|
{
|
|
nextDeltaCounter = c;
|
|
|
|
cpuSetNextEvent(nextStartCounter, nextDeltaCounter); // Need to update on counter resets/target changes
|
|
}
|
|
|
|
// Ignore target diff if target is currently disabled.
|
|
// (the overflow is all we care about since it goes first, and then the
|
|
// target will be turned on afterward, and handled in the next event test).
|
|
|
|
if (counter.target & EECNT_FUTURE_TARGET)
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
|
|
c = ((counter.target - counter.count) * counter.rate) - (cpuRegs.cycle - counter.startCycle);
|
|
c += cpuRegs.cycle - nextStartCounter; // adjust for time passed since last rcntUpdate();
|
|
|
|
if (c < nextDeltaCounter)
|
|
{
|
|
nextDeltaCounter = c;
|
|
cpuSetNextEvent(nextStartCounter, nextDeltaCounter); // Need to update on counter resets/target changes
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static __fi void cpuRcntSet()
|
|
{
|
|
int i;
|
|
|
|
// Default to next VBlank
|
|
nextStartCounter = cpuRegs.cycle;
|
|
nextDeltaCounter = vsyncCounter.deltaCycles - (cpuRegs.cycle - vsyncCounter.startCycle);
|
|
|
|
// Also check next HSync
|
|
s32 nextHsync = hsyncCounter.deltaCycles - (cpuRegs.cycle - hsyncCounter.startCycle);
|
|
if (nextHsync < nextDeltaCounter)
|
|
nextDeltaCounter = nextHsync;
|
|
|
|
for (i = 0; i < 4; i++)
|
|
_rcntSet(i);
|
|
|
|
// sanity check!
|
|
if (nextDeltaCounter < 0)
|
|
nextDeltaCounter = 0;
|
|
|
|
cpuSetNextEvent(nextStartCounter, nextDeltaCounter); // Need to update on counter resets/target changes
|
|
}
|
|
|
|
|
|
struct vSyncTimingInfo
|
|
{
|
|
double Framerate; // frames per second (8 bit fixed)
|
|
GS_VideoMode VideoMode; // used to detect change (interlaced/progressive)
|
|
u32 Render; // time from vblank end to vblank start (cycles)
|
|
u32 Blank; // time from vblank start to vblank end (cycles)
|
|
|
|
u32 GSBlank; // GS CSR is swapped roughly 3.5 hblank's after vblank start
|
|
|
|
u32 hSyncError; // rounding error after the duration of a rendered frame (cycles)
|
|
u32 hRender; // time from hblank end to hblank start (cycles)
|
|
u32 hBlank; // time from hblank start to hblank end (cycles)
|
|
u32 hScanlinesPerFrame; // number of scanlines per frame (525/625 for NTSC/PAL)
|
|
};
|
|
|
|
static vSyncTimingInfo vSyncInfo;
|
|
|
|
void rcntInit()
|
|
{
|
|
int i;
|
|
|
|
g_FrameCount = 0;
|
|
|
|
std::memset(counters, 0, sizeof(counters));
|
|
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
counters[i].rate = 2;
|
|
counters[i].target = 0xffff;
|
|
}
|
|
counters[0].interrupt = 9;
|
|
counters[1].interrupt = 10;
|
|
counters[2].interrupt = 11;
|
|
counters[3].interrupt = 12;
|
|
|
|
std::memset(&vSyncInfo, 0, sizeof(vSyncInfo));
|
|
|
|
gsVideoMode = GS_VideoMode::Uninitialized;
|
|
gsIsInterlaced = VMManager::Internal::IsFastBootInProgress();
|
|
|
|
hsyncCounter.Mode = MODE_HRENDER;
|
|
hsyncCounter.startCycle = cpuRegs.cycle;
|
|
hsyncCounter.deltaCycles = vSyncInfo.hRender;
|
|
vsyncCounter.Mode = MODE_VRENDER;
|
|
vsyncCounter.deltaCycles = vSyncInfo.Render;
|
|
vsyncCounter.startCycle = cpuRegs.cycle;
|
|
|
|
for (i = 0; i < 4; i++)
|
|
rcntReset(i);
|
|
cpuRcntSet();
|
|
}
|
|
|
|
static void vSyncInfoCalc(vSyncTimingInfo* info, double framesPerSecond, u32 scansPerFrame)
|
|
{
|
|
constexpr double clock = static_cast<double>(PS2CLK);
|
|
|
|
const u64 Frame = clock * 10000ULL / framesPerSecond;
|
|
const u64 Scanline = Frame / scansPerFrame;
|
|
|
|
// There are two renders and blanks per frame. This matches the PS2 test results.
|
|
// The PAL and NTSC VBlank periods respectively lasts for approximately 22 and 26 scanlines.
|
|
// An older test suggests that these periods are actually the periods that VBlank is off, but
|
|
// Legendz Gekitou! Saga Battle runs very slowly if the VBlank period is inverted.
|
|
// Some of the more timing sensitive games and their symptoms when things aren't right:
|
|
// Dynasty Warriors 3 Xtreme Legends - fake save corruption when loading save
|
|
// Jak II - random speedups
|
|
// Shadow of Rome - FMV audio issues
|
|
const bool ntsc_hblank = gsVideoMode != GS_VideoMode::PAL && gsVideoMode != GS_VideoMode::DVD_PAL;
|
|
const u64 HalfFrame = Frame / 2;
|
|
const float extra_scanlines = static_cast<float>(IsProgressiveVideoMode()) * (ntsc_hblank ? 0.5f : 1.5f);
|
|
const u64 Blank = Scanline * ((ntsc_hblank ? 22.5f : 24.5f) + extra_scanlines);
|
|
const u64 Render = HalfFrame - Blank;
|
|
const u64 GSBlank = Scanline * ((ntsc_hblank ? 3.5 : 3) + extra_scanlines); // GS VBlank/CSR Swap happens roughly 3.5(NTSC) and 3(PAL) Scanlines after VBlank Start
|
|
|
|
// Important! The hRender/hBlank timer ratio below is set according to PS2 tests.
|
|
// in EE Cycles taken from PAL system:
|
|
// 18876 cycles for hsync
|
|
// 15796 cycles for hsync are low (render)
|
|
// Ratio: 83.68298368298368
|
|
u64 hRender = Scanline * 0.8368298368298368f;
|
|
u64 hBlank = Scanline - hRender;
|
|
|
|
if (!IsInterlacedVideoMode())
|
|
{
|
|
hBlank /= 2;
|
|
hRender /= 2;
|
|
}
|
|
|
|
//TODO: Carry fixed-point math all the way through the entire vsync and hsync counting processes, and continually apply rounding
|
|
//as needed for each scheduled v/hsync related event. Much better to handle than this messed state.
|
|
info->Framerate = framesPerSecond;
|
|
info->GSBlank = (u32)(GSBlank / 10000);
|
|
info->Render = (u32)(Render / 10000);
|
|
info->Blank = (u32)(Blank / 10000);
|
|
const u64 accumilated_vrender = (Render % 10000) + (Blank % 10000);
|
|
info->Render += (u32)(accumilated_vrender / 10000);
|
|
|
|
info->hRender = (u32)(hRender / 10000);
|
|
info->hBlank = (u32)(hBlank / 10000);
|
|
info->hScanlinesPerFrame = scansPerFrame;
|
|
|
|
const u64 accumilatedHRenderError = (hRender % 10000) + (hBlank % 10000);
|
|
const u64 accumilatedHFractional = accumilatedHRenderError % 10000;
|
|
info->hRender += (u32)(accumilatedHRenderError / 10000);
|
|
info->hSyncError = (u32)((accumilatedHFractional * (scansPerFrame / (IsInterlacedVideoMode() ? 2 : 1))) / 10000);
|
|
|
|
// Note: In NTSC modes there is some small rounding error in the vsync too,
|
|
// however it would take thousands of frames for it to amount to anything and
|
|
// is thus not worth the effort at this time.
|
|
}
|
|
|
|
const char* ReportVideoMode()
|
|
{
|
|
switch (gsVideoMode)
|
|
{
|
|
case GS_VideoMode::PAL: return "PAL";
|
|
case GS_VideoMode::NTSC: return "NTSC";
|
|
case GS_VideoMode::DVD_NTSC: return "DVD NTSC";
|
|
case GS_VideoMode::DVD_PAL: return "DVD PAL";
|
|
case GS_VideoMode::VESA: return "VESA";
|
|
case GS_VideoMode::SDTV_480P: return "SDTV 480p";
|
|
case GS_VideoMode::SDTV_576P: return "SDTV 576p";
|
|
case GS_VideoMode::HDTV_720P: return "HDTV 720p";
|
|
case GS_VideoMode::HDTV_1080I: return "HDTV 1080i";
|
|
case GS_VideoMode::HDTV_1080P: return "HDTV 1080p";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
const char* ReportInterlaceMode()
|
|
{
|
|
const u64& smode2 = *(u64*)PS2GS_BASE(GS_SMODE2);
|
|
return !IsProgressiveVideoMode() ? ((smode2 & 2) ? "Interlaced (Frame)" : "Interlaced (Field)") : "Progressive";
|
|
}
|
|
|
|
double GetVerticalFrequency()
|
|
{
|
|
// Note about NTSC/PAL "double strike" modes:
|
|
// NTSC and PAL can be configured in such a way to produce a non-interlaced signal.
|
|
// This involves modifying the signal slightly by either adding or subtracting a line (526/524 instead of 525)
|
|
// which has the function of causing the odd and even fields to strike the same lines.
|
|
// Doing this modifies the vertical refresh rate slightly. Beatmania is sensitive to this and
|
|
// not accounting for it will cause the audio and video to become desynced.
|
|
//
|
|
// In the case of the GS, I believe it adds a halfline to the vertical back porch but more research is needed.
|
|
// For now I'm just going to subtract off the config setting.
|
|
//
|
|
// According to the GS:
|
|
// NTSC (interlaced): 59.94
|
|
// NTSC (non-interlaced): 59.82
|
|
// PAL (interlaced): 50.00
|
|
// PAL (non-interlaced): 49.76
|
|
//
|
|
// More Information:
|
|
// https://web.archive.org/web/20201031235528/https://wiki.nesdev.com/w/index.php/NTSC_video
|
|
// https://web.archive.org/web/20201102100937/http://forums.nesdev.com/viewtopic.php?t=7909
|
|
// https://web.archive.org/web/20120629231826fw_/http://ntsc-tv.com/index.html
|
|
// https://web.archive.org/web/20200831051302/https://www.hdretrovision.com/240p/
|
|
|
|
switch (gsVideoMode)
|
|
{
|
|
case GS_VideoMode::Uninitialized: // SetGsCrt hasn't executed yet, give some temporary values.
|
|
return 60.00;
|
|
case GS_VideoMode::PAL:
|
|
case GS_VideoMode::DVD_PAL:
|
|
return (IsProgressiveVideoMode() == false) ? EmuConfig.GS.FrameratePAL : EmuConfig.GS.FrameratePAL - 0.24f;
|
|
case GS_VideoMode::NTSC:
|
|
case GS_VideoMode::DVD_NTSC:
|
|
return (IsProgressiveVideoMode() == false) ? EmuConfig.GS.FramerateNTSC : EmuConfig.GS.FramerateNTSC - 0.11f;
|
|
case GS_VideoMode::SDTV_480P:
|
|
return 59.94;
|
|
case GS_VideoMode::HDTV_1080P:
|
|
case GS_VideoMode::HDTV_1080I:
|
|
case GS_VideoMode::HDTV_720P:
|
|
case GS_VideoMode::SDTV_576P:
|
|
case GS_VideoMode::VESA:
|
|
return 60.00;
|
|
default:
|
|
// Pass NTSC vertical frequency value when unknown video mode is detected.
|
|
return FRAMERATE_NTSC * 2;
|
|
}
|
|
}
|
|
|
|
void UpdateVSyncRate(bool force)
|
|
{
|
|
// Notice: (and I probably repeat this elsewhere, but it's worth repeating)
|
|
// The PS2's vsync timer is an *independent* crystal that is fixed to either 59.94 (NTSC)
|
|
// or 50.0 (PAL) Hz. It has *nothing* to do with real TV timings or the real vsync of
|
|
// the GS's output circuit. It is the same regardless if the GS is outputting interlace
|
|
// or progressive scan content.
|
|
|
|
const double vertical_frequency = GetVerticalFrequency();
|
|
|
|
const double frames_per_second = vertical_frequency / 2.0;
|
|
|
|
if (vSyncInfo.Framerate != frames_per_second || vSyncInfo.VideoMode != gsVideoMode || force)
|
|
{
|
|
u32 total_scanlines = 0;
|
|
bool custom = false;
|
|
|
|
switch (gsVideoMode)
|
|
{
|
|
case GS_VideoMode::Uninitialized: // SYSCALL instruction hasn't executed yet, give some temporary values.
|
|
if (gsIsInterlaced)
|
|
total_scanlines = SCANLINES_TOTAL_NTSC_I;
|
|
else
|
|
total_scanlines = SCANLINES_TOTAL_NTSC_NI;
|
|
break;
|
|
case GS_VideoMode::PAL:
|
|
case GS_VideoMode::DVD_PAL:
|
|
custom = (EmuConfig.GS.FrameratePAL != Pcsx2Config::GSOptions::DEFAULT_FRAME_RATE_PAL);
|
|
if (gsIsInterlaced)
|
|
total_scanlines = SCANLINES_TOTAL_PAL_I;
|
|
else
|
|
total_scanlines = SCANLINES_TOTAL_PAL_NI;
|
|
break;
|
|
case GS_VideoMode::NTSC:
|
|
case GS_VideoMode::DVD_NTSC:
|
|
custom = (EmuConfig.GS.FramerateNTSC != Pcsx2Config::GSOptions::DEFAULT_FRAME_RATE_NTSC);
|
|
if (gsIsInterlaced)
|
|
total_scanlines = SCANLINES_TOTAL_NTSC_I;
|
|
else
|
|
total_scanlines = SCANLINES_TOTAL_NTSC_NI;
|
|
break;
|
|
case GS_VideoMode::SDTV_480P:
|
|
case GS_VideoMode::SDTV_576P:
|
|
case GS_VideoMode::HDTV_720P:
|
|
case GS_VideoMode::VESA:
|
|
total_scanlines = SCANLINES_TOTAL_NTSC_I;
|
|
break;
|
|
case GS_VideoMode::HDTV_1080P:
|
|
case GS_VideoMode::HDTV_1080I:
|
|
total_scanlines = SCANLINES_TOTAL_1080;
|
|
break;
|
|
case GS_VideoMode::Unknown:
|
|
default:
|
|
if (gsIsInterlaced)
|
|
total_scanlines = SCANLINES_TOTAL_NTSC_I;
|
|
else
|
|
total_scanlines = SCANLINES_TOTAL_NTSC_NI;
|
|
Console.Error("PCSX2-Counters: Unknown video mode detected");
|
|
pxAssertMsg(false, "Unknown video mode detected via SetGsCrt");
|
|
}
|
|
|
|
const bool video_mode_initialized = gsVideoMode != GS_VideoMode::Uninitialized;
|
|
|
|
// NBA Jam 2004 PAL will fail to display 3D on the menu if this value isn't correct on reset.
|
|
if (video_mode_initialized && vSyncInfo.VideoMode != gsVideoMode)
|
|
CSRreg.FIELD = 1;
|
|
|
|
vSyncInfo.VideoMode = gsVideoMode;
|
|
|
|
vSyncInfoCalc(&vSyncInfo, frames_per_second, total_scanlines);
|
|
|
|
if (video_mode_initialized)
|
|
Console.WriteLn(Color_Green, "UpdateVSyncRate: Mode Changed to %s.", ReportVideoMode());
|
|
|
|
if (custom && video_mode_initialized)
|
|
Console.WriteLn(Color_StrongGreen, " ... with user configured refresh rate: %.02f Hz", vertical_frequency);
|
|
|
|
s32 hdiff = hsyncCounter.deltaCycles;
|
|
s32 vdiff = vsyncCounter.deltaCycles;
|
|
hsyncCounter.deltaCycles = (hsyncCounter.Mode == MODE_HBLANK) ? vSyncInfo.hBlank : vSyncInfo.hRender;
|
|
vsyncCounter.deltaCycles = (vsyncCounter.Mode == MODE_GSBLANK) ?
|
|
vSyncInfo.GSBlank :
|
|
((vsyncCounter.Mode == MODE_VBLANK) ? vSyncInfo.Blank : vSyncInfo.Render);
|
|
|
|
hsyncCounter.startCycle += hdiff - hsyncCounter.deltaCycles;
|
|
vsyncCounter.startCycle += vdiff - vsyncCounter.deltaCycles;
|
|
|
|
cpuRcntSet();
|
|
|
|
VMManager::Internal::FrameRateChanged();
|
|
}
|
|
}
|
|
|
|
// FMV switch stuff
|
|
extern u64 eecount_on_last_vdec;
|
|
extern bool FMVstarted;
|
|
extern bool EnableFMV;
|
|
|
|
static bool s_last_fmv_state = false;
|
|
|
|
static __fi void DoFMVSwitch()
|
|
{
|
|
bool new_fmv_state = s_last_fmv_state;
|
|
if (EnableFMV)
|
|
{
|
|
DevCon.WriteLn("FMV started");
|
|
new_fmv_state = true;
|
|
EnableFMV = false;
|
|
}
|
|
else if (FMVstarted)
|
|
{
|
|
const int diff = cpuRegs.cycle - eecount_on_last_vdec;
|
|
if (diff > 60000000)
|
|
{
|
|
DevCon.WriteLn("FMV ended");
|
|
new_fmv_state = false;
|
|
FMVstarted = false;
|
|
}
|
|
}
|
|
|
|
if (new_fmv_state == s_last_fmv_state)
|
|
return;
|
|
|
|
s_last_fmv_state = new_fmv_state;
|
|
|
|
switch (EmuConfig.GS.FMVAspectRatioSwitch)
|
|
{
|
|
case FMVAspectRatioSwitchType::Off:
|
|
break;
|
|
case FMVAspectRatioSwitchType::RAuto4_3_3_2:
|
|
EmuConfig.CurrentAspectRatio = new_fmv_state ? AspectRatioType::RAuto4_3_3_2 : EmuConfig.GS.AspectRatio;
|
|
break;
|
|
case FMVAspectRatioSwitchType::R4_3:
|
|
EmuConfig.CurrentAspectRatio = new_fmv_state ? AspectRatioType::R4_3 : EmuConfig.GS.AspectRatio;
|
|
break;
|
|
case FMVAspectRatioSwitchType::R16_9:
|
|
EmuConfig.CurrentAspectRatio = new_fmv_state ? AspectRatioType::R16_9 : EmuConfig.GS.AspectRatio;
|
|
break;
|
|
case FMVAspectRatioSwitchType::R10_7:
|
|
EmuConfig.CurrentAspectRatio = new_fmv_state ? AspectRatioType::R10_7 : EmuConfig.GS.AspectRatio;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (EmuConfig.Gamefixes.SoftwareRendererFMVHack && EmuConfig.GS.UseHardwareRenderer())
|
|
{
|
|
DevCon.Warning("FMV Switch");
|
|
// we don't use the sw toggle here, because it'll change back to auto if set to sw
|
|
MTGS::SetSoftwareRendering(new_fmv_state, new_fmv_state ? GSInterlaceMode::AdaptiveTFF : EmuConfig.GS.InterlaceMode, false);
|
|
}
|
|
}
|
|
|
|
static __fi void VSyncStart(u64 sCycle)
|
|
{
|
|
// End-of-frame tasks.
|
|
DoFMVSwitch();
|
|
VMManager::Internal::VSyncOnCPUThread();
|
|
|
|
// Don't bother throttling if we're going to pause.
|
|
if (!VMManager::Internal::IsExecutionInterrupted())
|
|
VMManager::Internal::Throttle();
|
|
|
|
gsPostVsyncStart(); // MUST be after framelimit; doing so before causes funk with frame times!
|
|
|
|
// Poll input after MTGS frame push, just in case it has to stall to catch up.
|
|
VMManager::Internal::PollInputOnCPUThread();
|
|
|
|
EECNT_LOG(" ================ EE COUNTER VSYNC START (frame: %d) ================", g_FrameCount);
|
|
|
|
// Memcard auto ejection - Uses a tick system timed off of real time, decrementing one tick per frame.
|
|
AutoEject::CountDownTicks();
|
|
// Memcard IO detection - Uses a tick system to determine when memcards are no longer being written.
|
|
MemcardBusy::Decrement();
|
|
|
|
if (!GSSMODE1reg.SINT)
|
|
{
|
|
hwIntcIrq(INTC_VBLANK_S);
|
|
rcntStartGate(true, sCycle); // Counters Start Gate code
|
|
psxVBlankStart();
|
|
}
|
|
|
|
// INTC - VB Blank Start Hack --
|
|
// Hack fix! This corrects a freezeup in Granda 2 where it decides to spin
|
|
// on the INTC_STAT register after the exception handler has already cleared
|
|
// it. But be warned! Set the value to larger than 4 and it breaks Dark
|
|
// Cloud and other games. -_-
|
|
|
|
// How it works: Normally the INTC raises exceptions immediately at the end of the
|
|
// current branch test. But in the case of Grandia 2, the game's code is spinning
|
|
// on the INTC status, and the exception handler (for some reason?) clears the INTC
|
|
// before returning *and* returns to a location other than EPC. So the game never
|
|
// gets to the point where it sees the INTC Irq set true.
|
|
|
|
// (I haven't investigated why Dark Cloud freezes on larger values)
|
|
// (all testing done using the recompiler -- dunno how the ints respond yet)
|
|
|
|
//cpuRegs.eCycle[30] = 2;
|
|
|
|
// Update 08/2021: The only game I know to require this kind of thing as of 1.7.0 is Penny Racers/Gadget Racers (which has a patch to avoid the problem and others)
|
|
// These games have a tight loop checking INTC_STAT waiting for the VBLANK Start, however the game also has a VBLANK Hander which clears it.
|
|
// Therefore, there needs to be some delay in order for it to see the interrupt flag before the interrupt is acknowledged, likely helped on real hardware by the pipelines.
|
|
// Without the patch and fixing this, the games have other issues, so I'm not going to rush to fix it.
|
|
// Refraction
|
|
|
|
// Bail out before the next frame starts if we're paused, or the CPU has changed.
|
|
// Need to re-check this, because we might've paused during the sleep time.
|
|
if (VMManager::Internal::IsExecutionInterrupted())
|
|
Cpu->ExitExecution();
|
|
}
|
|
|
|
static __fi void GSVSync()
|
|
{
|
|
// CSR is swapped and GS vBlank IRQ is triggered roughly 3.5 hblanks after VSync Start
|
|
if (GSSMODE1reg.SINT)
|
|
return;
|
|
|
|
if (IsProgressiveVideoMode())
|
|
CSRreg.SetField();
|
|
else
|
|
CSRreg.SwapField();
|
|
|
|
if (!CSRreg.VSINT)
|
|
{
|
|
CSRreg.VSINT = true;
|
|
if (!GSIMR.VSMSK)
|
|
gsIrq();
|
|
}
|
|
}
|
|
|
|
static __fi void VSyncEnd(u64 sCycle)
|
|
{
|
|
EECNT_LOG(" ================ EE COUNTER VSYNC END (frame: %d) ================", g_FrameCount);
|
|
|
|
g_FrameCount++;
|
|
if (!GSSMODE1reg.SINT)
|
|
{
|
|
hwIntcIrq(INTC_VBLANK_E); // HW Irq
|
|
psxVBlankEnd(); // psxCounters vBlank End
|
|
rcntEndGate(true, sCycle); // Counters End Gate Code
|
|
}
|
|
|
|
// This doesn't seem to be needed here. Games only seem to break with regard to the
|
|
// vsyncstart irq.
|
|
//cpuRegs.eCycle[30] = 2;
|
|
}
|
|
|
|
//#define VSYNC_DEBUG // Uncomment this to enable some vSync Timer debugging features.
|
|
#ifdef VSYNC_DEBUG
|
|
static u32 hsc = 0;
|
|
static int vblankinc = 0;
|
|
#endif
|
|
|
|
__fi void rcntUpdate_vSync()
|
|
{
|
|
if (!cpuTestCycle(vsyncCounter.startCycle, vsyncCounter.deltaCycles))
|
|
return;
|
|
|
|
if (vsyncCounter.Mode == MODE_VBLANK)
|
|
{
|
|
vsyncCounter.startCycle += vSyncInfo.Blank;
|
|
vsyncCounter.deltaCycles = vSyncInfo.Render;
|
|
|
|
VSyncEnd(vsyncCounter.startCycle);
|
|
|
|
vsyncCounter.Mode = MODE_VRENDER; // VSYNC END - Render begin
|
|
}
|
|
else if (vsyncCounter.Mode == MODE_GSBLANK) // GS CSR Swap and interrupt
|
|
{
|
|
GSVSync();
|
|
|
|
vsyncCounter.Mode = MODE_VBLANK;
|
|
// Don't set the start cycle, makes it easier to calculate the correct Vsync End time
|
|
vsyncCounter.deltaCycles = vSyncInfo.Blank;
|
|
}
|
|
else // VSYNC Start
|
|
{
|
|
vsyncCounter.startCycle += vSyncInfo.Render;
|
|
vsyncCounter.deltaCycles = vSyncInfo.GSBlank;
|
|
|
|
VSyncStart(vsyncCounter.startCycle);
|
|
|
|
vsyncCounter.Mode = MODE_GSBLANK;
|
|
|
|
// Accumulate hsync rounding errors:
|
|
hsyncCounter.deltaCycles += vSyncInfo.hSyncError;
|
|
|
|
#ifdef VSYNC_DEBUG
|
|
vblankinc++;
|
|
if (vblankinc > 1)
|
|
{
|
|
if (hsc != vSyncInfo.hScanlinesPerFrame)
|
|
Console.WriteLn(" ** vSync > Abnormal Scanline Count: %d", hsc);
|
|
hsc = 0;
|
|
vblankinc = 0;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
__fi void rcntUpdate_hScanline()
|
|
{
|
|
if (!cpuTestCycle(hsyncCounter.startCycle, hsyncCounter.deltaCycles))
|
|
return;
|
|
|
|
//iopEventAction = 1;
|
|
if (hsyncCounter.Mode == MODE_HBLANK)
|
|
{ //HBLANK End / HRENDER Begin
|
|
|
|
// Setup the hRender's start and end cycle information:
|
|
hsyncCounter.startCycle += vSyncInfo.hBlank; // start (absolute cycle value)
|
|
hsyncCounter.deltaCycles = vSyncInfo.hRender; // endpoint (delta from start value)
|
|
if (!GSSMODE1reg.SINT)
|
|
{
|
|
rcntEndGate(false, hsyncCounter.startCycle);
|
|
psxHBlankEnd();
|
|
}
|
|
|
|
hsyncCounter.Mode = MODE_HRENDER;
|
|
}
|
|
else
|
|
{ //HBLANK START / HRENDER End
|
|
|
|
// set up the hblank's start and end cycle information:
|
|
hsyncCounter.startCycle += vSyncInfo.hRender; // start (absolute cycle value)
|
|
hsyncCounter.deltaCycles = vSyncInfo.hBlank; // endpoint (delta from start value)
|
|
if (!GSSMODE1reg.SINT)
|
|
{
|
|
if (!CSRreg.HSINT)
|
|
{
|
|
CSRreg.HSINT = true;
|
|
if (!GSIMR.HSMSK)
|
|
gsIrq();
|
|
}
|
|
|
|
rcntStartGate(false, hsyncCounter.startCycle);
|
|
psxHBlankStart();
|
|
}
|
|
|
|
hsyncCounter.Mode = MODE_HBLANK;
|
|
|
|
#ifdef VSYNC_DEBUG
|
|
hsc++;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
static __fi void _cpuTestTarget(int i)
|
|
{
|
|
if (counters[i].count < counters[i].target)
|
|
return;
|
|
|
|
if (counters[i].mode.TargetInterrupt)
|
|
{
|
|
EECNT_LOG("EE Counter[%d] TARGET reached - mode=%x, count=%x, target=%x", i, counters[i].mode, counters[i].count, counters[i].target);
|
|
if (!counters[i].mode.TargetReached)
|
|
{
|
|
counters[i].mode.TargetReached = 1;
|
|
hwIntcIrq(counters[i].interrupt);
|
|
}
|
|
}
|
|
|
|
if (counters[i].mode.ZeroReturn)
|
|
counters[i].count -= counters[i].target; // Reset on target
|
|
else
|
|
counters[i].target |= EECNT_FUTURE_TARGET; // OR with future target to prevent a retrigger
|
|
}
|
|
|
|
static __fi void _cpuTestOverflow(int i)
|
|
{
|
|
if (counters[i].count <= 0xffff)
|
|
return;
|
|
|
|
if (counters[i].mode.OverflowInterrupt)
|
|
{
|
|
EECNT_LOG("EE Counter[%d] OVERFLOW - mode=%x, count=%x", i, counters[i].mode, counters[i].count);
|
|
if (!counters[i].mode.OverflowReached)
|
|
{
|
|
counters[i].mode.OverflowReached = 1;
|
|
hwIntcIrq(counters[i].interrupt);
|
|
}
|
|
}
|
|
|
|
// wrap counter back around zero, and enable the future target:
|
|
counters[i].count -= 0x10000;
|
|
counters[i].target &= 0xffff;
|
|
}
|
|
|
|
|
|
__fi bool rcntCanCount(int i)
|
|
{
|
|
if (!counters[i].mode.IsCounting)
|
|
return false;
|
|
|
|
if (!counters[i].mode.EnableGate)
|
|
return true;
|
|
|
|
// If we're in gate mode, we can only count if it's not both gated and counting on HBLANK or GateMode is not 0 (Count only when low) or the signal is low.
|
|
return ((counters[i].mode.GateSource == 0 && counters[i].mode.ClockSource != 3 && (hsyncCounter.Mode == MODE_HRENDER || counters[i].mode.GateMode != 0)) ||
|
|
(counters[i].mode.GateSource == 1 && (vsyncCounter.Mode == MODE_VRENDER || counters[i].mode.GateMode != 0)));
|
|
}
|
|
|
|
__fi void rcntSyncCounter(int i)
|
|
{
|
|
if (counters[i].mode.ClockSource != 0x3) // don't count hblank sources
|
|
{
|
|
const u32 change = (cpuRegs.cycle - counters[i].startCycle) / counters[i].rate;
|
|
counters[i].startCycle += change * counters[i].rate;
|
|
|
|
counters[i].startCycle &= ~((u64)counters[i].rate - 1);
|
|
|
|
if (rcntCanCount(i))
|
|
counters[i].count += change;
|
|
}
|
|
else
|
|
counters[i].startCycle = cpuRegs.cycle;
|
|
}
|
|
|
|
// forceinline note: this method is called from two locations, but one
|
|
// of them is the interpreter, which doesn't count. ;) So might as
|
|
// well forceinline it!
|
|
__fi void rcntUpdate()
|
|
{
|
|
rcntUpdate_vSync();
|
|
// HBlank after as VSync can do error compensation
|
|
rcntUpdate_hScanline();
|
|
|
|
// Update counters so that we can perform overflow and target tests.
|
|
|
|
for (int i = 0; i <= 3; i++)
|
|
{
|
|
rcntSyncCounter(i);
|
|
|
|
if (counters[i].mode.ClockSource == 0x3 || !rcntCanCount(i)) // don't count hblank sources
|
|
continue;
|
|
|
|
_cpuTestOverflow(i);
|
|
_cpuTestTarget(i);
|
|
}
|
|
|
|
cpuRcntSet();
|
|
}
|
|
|
|
static __fi void _rcntSetGate(int index)
|
|
{
|
|
if (counters[index].mode.EnableGate)
|
|
{
|
|
// If the Gate Source is hblank and the clock selection is also hblank
|
|
// the timer completely turns off (HW Tested).
|
|
if (!(counters[index].mode.GateSource == 0 && counters[index].mode.ClockSource == 3))
|
|
EECNT_LOG("EE Counter[%d] Using Gate! Source=%s, Mode=%d.",
|
|
index, counters[index].mode.GateSource ? "vblank" : "hblank", counters[index].mode.GateMode);
|
|
else
|
|
EECNT_LOG("EE Counter[%d] GATE DISABLED because of hblank source.", index);
|
|
}
|
|
}
|
|
|
|
// mode - 0 means hblank source, 8 means vblank source.
|
|
static __fi void rcntStartGate(bool isVblank, u64 sCycle)
|
|
{
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
if (!isVblank && (counters[i].mode.ClockSource == 3) && rcntCanCount(i))
|
|
{
|
|
// Update counters using the hblank as the clock. This keeps the hblank source
|
|
// nicely in sync with the counters and serves as an optimization also, since these
|
|
// counter won't receive special rcntUpdate scheduling.
|
|
// Note: Target and overflow tests must be done here since they won't be done
|
|
// currectly by rcntUpdate (since it's not being scheduled for these counters)
|
|
counters[i].count += HBLANK_COUNTER_SPEED;
|
|
_cpuTestOverflow(i);
|
|
_cpuTestTarget(i);
|
|
}
|
|
|
|
if (!counters[i].mode.EnableGate)
|
|
continue;
|
|
|
|
if ((!!counters[i].mode.GateSource) != isVblank)
|
|
continue;
|
|
|
|
switch (counters[i].mode.GateMode)
|
|
{
|
|
case 0x0: // Count When Signal is low (V_RENDER ONLY)
|
|
|
|
// Just set the start cycle -- counting will be done as needed
|
|
// for events (overflows, targets, mode changes, and the gate off below)
|
|
rcntSyncCounter(i);
|
|
counters[i].startCycle = sCycle & ~((u64)counters[i].rate - 1);
|
|
EECNT_LOG("EE Counter[%d] %s StartGate Type0, count = %x", i,
|
|
isVblank ? "vblank" : "hblank", counters[i].count);
|
|
break;
|
|
case 0x2: // Reset on Vsync end
|
|
// This is the vsync start so do nothing.
|
|
break;
|
|
case 0x1: // Reset on Vsync start
|
|
case 0x3: // Reset on Vsync start and end
|
|
rcntSyncCounter(i);
|
|
counters[i].count = 0;
|
|
counters[i].target &= 0xffff;
|
|
counters[i].startCycle = sCycle & ~((u64)counters[i].rate - 1);
|
|
EECNT_LOG("EE Counter[%d] %s StartGate Type%d, count = %x", i,
|
|
isVblank ? "vblank" : "hblank", counters[i].mode.GateMode, counters[i].count);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// No need to update actual counts here. Counts are calculated as needed by reads to
|
|
// rcntRcount(). And so long as sCycleT is set properly, any targets or overflows
|
|
// will be scheduled and handled.
|
|
|
|
// Note: No need to set counters here. They'll get set when control returns to
|
|
// rcntUpdate, since we're being called from there anyway.
|
|
}
|
|
|
|
// mode - 0 means hblank signal, 8 means vblank signal.
|
|
static __fi void rcntEndGate(bool isVblank, u64 sCycle)
|
|
{
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
if (!counters[i].mode.EnableGate)
|
|
continue;
|
|
|
|
if ((!!counters[i].mode.GateSource) != isVblank)
|
|
continue;
|
|
|
|
switch (counters[i].mode.GateMode)
|
|
{
|
|
case 0x0: //Count When Signal is low (V_RENDER ONLY)
|
|
counters[i].startCycle = sCycle & ~((u64)counters[i].rate - 1);
|
|
|
|
EECNT_LOG("EE Counter[%d] %s EndGate Type0, count = %x", i,
|
|
isVblank ? "vblank" : "hblank", counters[i].count);
|
|
break;
|
|
|
|
case 0x1: // Reset on Vsync start
|
|
// This is the vsync end so do nothing
|
|
break;
|
|
|
|
case 0x2: // Reset on Vsync end
|
|
case 0x3: // Reset on Vsync start and end
|
|
rcntSyncCounter(i);
|
|
EECNT_LOG("EE Counter[%d] %s EndGate Type%d, count = %x", i,
|
|
isVblank ? "vblank" : "hblank", counters[i].mode.GateMode, counters[i].count);
|
|
counters[i].count = 0;
|
|
counters[i].target &= 0xffff;
|
|
counters[i].startCycle = sCycle & ~(static_cast<u64>(counters[i].rate - 1));
|
|
break;
|
|
}
|
|
}
|
|
// Note: No need to set counters here. They'll get set when control returns to
|
|
// rcntUpdate, since we're being called from there anyway.
|
|
}
|
|
|
|
static __fi void rcntWmode(int index, u32 value)
|
|
{
|
|
rcntSyncCounter(index);
|
|
|
|
// Clear OverflowReached and TargetReached flags (0xc00 mask), but *only* if they are set to 1 in the
|
|
// given value. (yes, the bits are cleared when written with '1's).
|
|
|
|
counters[index].modeval &= ~(value & 0xc00);
|
|
counters[index].modeval = (counters[index].modeval & 0xc00) | (value & 0x3ff);
|
|
EECNT_LOG("EE Counter[%d] writeMode = %x passed value=%x", index, counters[index].modeval, value);
|
|
|
|
switch (counters[index].mode.ClockSource) { //Clock rate divisers *2, they use BUSCLK speed not PS2CLK
|
|
case 0: counters[index].rate = 2; break;
|
|
case 1: counters[index].rate = 32; break;
|
|
case 2: counters[index].rate = 512; break;
|
|
case 3: counters[index].rate = vSyncInfo.hBlank+vSyncInfo.hRender; break;
|
|
}
|
|
|
|
// In case the rate has changed we need to set the start cycle to the previous tick.
|
|
counters[index].startCycle = cpuRegs.cycle & ~((u64)counters[index].rate - 1);
|
|
_rcntSetGate(index);
|
|
_rcntSet(index);
|
|
}
|
|
|
|
static __fi void rcntWcount(int index, u32 value)
|
|
{
|
|
EECNT_LOG("EE Counter[%d] writeCount = %x, oldcount=%x, target=%x", index, value, counters[index].count, counters[index].target);
|
|
|
|
// re-calculate the start cycle of the counter based on elapsed time since the last counter update:
|
|
rcntSyncCounter(index);
|
|
|
|
counters[index].count = value & 0xffff;
|
|
|
|
// reset the target, and make sure we don't get a premature target.
|
|
counters[index].target &= 0xffff;
|
|
|
|
if (counters[index].count >= counters[index].target)
|
|
counters[index].target |= EECNT_FUTURE_TARGET;
|
|
|
|
_rcntSet(index);
|
|
}
|
|
|
|
static __fi void rcntWtarget(int index, u32 value)
|
|
{
|
|
EECNT_LOG("EE Counter[%d] writeTarget = %x", index, value);
|
|
|
|
counters[index].target = value & 0xffff;
|
|
|
|
// guard against premature (instant) targeting.
|
|
// If the target is behind the current count, set it up so that the counter must
|
|
// overflow first before the target fires:
|
|
|
|
rcntSyncCounter(index);
|
|
|
|
if (counters[index].target <= counters[index].count)
|
|
counters[index].target |= EECNT_FUTURE_TARGET;
|
|
|
|
_rcntSet(index);
|
|
}
|
|
|
|
static __fi void rcntWhold(int index, u32 value)
|
|
{
|
|
EECNT_LOG("EE Counter[%d] Hold Write = %x", index, value);
|
|
counters[index].hold = value;
|
|
}
|
|
|
|
__fi u32 rcntRcount(int index)
|
|
{
|
|
u32 ret;
|
|
|
|
rcntSyncCounter(index);
|
|
|
|
ret = counters[index].count;
|
|
|
|
// Never expose a boundary crossing (wrap or target-reset) to the guest
|
|
// before the corresponding interrupt has actually been DELIVERED. On
|
|
// hardware the boundary and the interrupt are the same edge, and with
|
|
// interrupts enabled the handler preempts before any later read can
|
|
// execute — a wrapped count paired with the pre-overflow ISR state is an
|
|
// impossible observation. Under the JITs that window is real and spans
|
|
// two phases:
|
|
// 1. The count (derived from the live cpuRegs.cycle) has crossed the
|
|
// boundary but the scheduled rcntUpdate event hasn't run yet.
|
|
// 2. rcntUpdate has processed the crossing (count wrapped, OVFF/EQUF
|
|
// set, INTC raised) but the exception is still waiting for the next
|
|
// event test to be dispatched, so the guest's ISR hasn't run.
|
|
// NFL 2K5's lock-free 64-bit clock (overflow-ISR-maintained wrap
|
|
// accumulator + T0_COUNT) reads time going backwards in that window and
|
|
// hangs in a runaway divide at the boot logo. Clamp the read to
|
|
// just-before-the-boundary until delivery. The deliverability guard makes
|
|
// this exact: with interrupts blocked (DI/EXL — including inside the
|
|
// handler itself) or the INTC source masked, the guest legitimately
|
|
// observes the wrapped count, as on hardware.
|
|
const u32 target = counters[index].target & 0xffff;
|
|
const bool intc_pending_delivery =
|
|
(psHu32(INTC_STAT) & psHu32(INTC_MASK) & (1u << counters[index].interrupt)) &&
|
|
(cpuRegs.CP0.n.Status.val & 0x400) &&
|
|
cpuRegs.CP0.n.Status.b.EIE && cpuRegs.CP0.n.Status.b.IE &&
|
|
!cpuRegs.CP0.n.Status.b.EXL && !cpuRegs.CP0.n.Status.b.ERL;
|
|
if (counters[index].mode.ZeroReturn)
|
|
{
|
|
if (target != 0 && (ret >= target || (counters[index].mode.TargetReached && intc_pending_delivery)))
|
|
ret = target - 1;
|
|
}
|
|
else if (ret > 0xffff || (counters[index].mode.OverflowReached && intc_pending_delivery))
|
|
{
|
|
ret = 0xffff;
|
|
}
|
|
|
|
// Spams the Console.
|
|
EECNT_LOG("EE Counter[%d] readCount32 = %x", index, ret);
|
|
return (u16)ret;
|
|
}
|
|
|
|
template <uint page>
|
|
__fi u16 rcntRead32(u32 mem)
|
|
{
|
|
// Important DevNote:
|
|
// Yes this uses a u16 return value on purpose! The upper bits 16 of the counter registers
|
|
// are all fixed to 0, so we always truncate everything in these two pages using a u16
|
|
// return value! --air
|
|
|
|
switch( mem )
|
|
{
|
|
case(RCNT0_COUNT): return (u16)rcntRcount(0);
|
|
case(RCNT0_MODE): return (u16)counters[0].modeval;
|
|
case(RCNT0_TARGET): return (u16)counters[0].target;
|
|
case(RCNT0_HOLD): return (u16)counters[0].hold;
|
|
|
|
case(RCNT1_COUNT): return (u16)rcntRcount(1);
|
|
case(RCNT1_MODE): return (u16)counters[1].modeval;
|
|
case(RCNT1_TARGET): return (u16)counters[1].target;
|
|
case(RCNT1_HOLD): return (u16)counters[1].hold;
|
|
|
|
case(RCNT2_COUNT): return (u16)rcntRcount(2);
|
|
case(RCNT2_MODE): return (u16)counters[2].modeval;
|
|
case(RCNT2_TARGET): return (u16)counters[2].target;
|
|
|
|
case(RCNT3_COUNT): return (u16)rcntRcount(3);
|
|
case(RCNT3_MODE): return (u16)counters[3].modeval;
|
|
case(RCNT3_TARGET): return (u16)counters[3].target;
|
|
}
|
|
|
|
return psHu16(mem);
|
|
}
|
|
|
|
template <uint page>
|
|
__fi bool rcntWrite32(u32 mem, mem32_t& value)
|
|
{
|
|
pxAssume(mem >= RCNT0_COUNT && mem < 0x10002000);
|
|
|
|
// [TODO] : counters should actually just use the EE's hw register space for storing
|
|
// count, mode, target, and hold. This will allow for a simplified handler for register
|
|
// reads.
|
|
|
|
switch( mem )
|
|
{
|
|
case(RCNT0_COUNT): return rcntWcount(0, value), false;
|
|
case(RCNT0_MODE): return rcntWmode(0, value), false;
|
|
case(RCNT0_TARGET): return rcntWtarget(0, value), false;
|
|
case(RCNT0_HOLD): return rcntWhold(0, value), false;
|
|
|
|
case(RCNT1_COUNT): return rcntWcount(1, value), false;
|
|
case(RCNT1_MODE): return rcntWmode(1, value), false;
|
|
case(RCNT1_TARGET): return rcntWtarget(1, value), false;
|
|
case(RCNT1_HOLD): return rcntWhold(1, value), false;
|
|
|
|
case(RCNT2_COUNT): return rcntWcount(2, value), false;
|
|
case(RCNT2_MODE): return rcntWmode(2, value), false;
|
|
case(RCNT2_TARGET): return rcntWtarget(2, value), false;
|
|
|
|
case(RCNT3_COUNT): return rcntWcount(3, value), false;
|
|
case(RCNT3_MODE): return rcntWmode(3, value), false;
|
|
case(RCNT3_TARGET): return rcntWtarget(3, value), false;
|
|
}
|
|
|
|
// unhandled .. do memory writeback.
|
|
return true;
|
|
}
|
|
|
|
template u16 rcntRead32<0x00>(u32 mem);
|
|
template u16 rcntRead32<0x01>(u32 mem);
|
|
|
|
template bool rcntWrite32<0x00>(u32 mem, mem32_t& value);
|
|
template bool rcntWrite32<0x01>(u32 mem, mem32_t& value);
|
|
|
|
bool SaveStateBase::rcntFreeze()
|
|
{
|
|
Freeze(counters);
|
|
Freeze(hsyncCounter);
|
|
Freeze(vsyncCounter);
|
|
Freeze(nextDeltaCounter);
|
|
Freeze(nextStartCounter);
|
|
Freeze(vSyncInfo);
|
|
Freeze(gsVideoMode);
|
|
Freeze(gsIsInterlaced);
|
|
|
|
if (IsLoading())
|
|
cpuRcntSet();
|
|
|
|
return IsOkay();
|
|
}
|