GV7-0e: PCRTC_SYNC + VSYNC records with inline executors

PCRTCDisplays is not vsync-only state — the HW Draw() heuristics read it
per draw — so under the split it is duplicated front/back and refreshed
by a PCRTC_SYNC record carrying the whole digested GSPCRTCRegs plus the
pre-decrement scanmask counter. GSvsync now digests (unchanged), submits
the PCRTC record, flushes, then submits a VSYNC record carrying
field/registers_written/idle_frame; the executor runs the whole VSync()
body (Merge, present, capture). Record order reproduces today's
semantics: vsync-flushed draws see the fresh display state, mid-frame
draws the previous frame's. Merge's scanmask decrement stays back-side;
the front mirrors it at enqueue once the copies are distinct (GV7-1).

GSPCRTCRegs hoists to GSBackQueue.h as the record payload type; GSState
keeps an alias.

Gate: gs_vertex_tests 17/17; gsrunner PNG hashes bit-identical to the
GV-0 baselines on all dumps, Vulkan and SW.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Brian Degenhardt
2026-07-19 11:53:04 -07:00
co-authored by Claude
parent f7816e589c
commit 7e612f4750
6 changed files with 138 additions and 68 deletions
+5 -1
View File
@@ -455,10 +455,14 @@ void GSvsync(u32 field, bool registers_written)
g_gs_renderer->PCRTCDisplays.CalculateDisplayOffset(g_gs_renderer->m_scanmask_used);
g_gs_renderer->PCRTCDisplays.CalculateFramebufferOffset(g_gs_renderer->m_scanmask_used, g_gs_renderer->m_regs->DISP[0].DISPFB, g_gs_renderer->m_regs->DISP[1].DISPFB);
// The PCRTC record must precede the vsync-flushed draw records — those draws
// see the fresh display state, mid-frame draws saw the previous frame's.
g_gs_renderer->SubmitPcrtcSync();
// Do not move the flush into the VSync() method. It's here because EE transfers
// get cleared in HW VSync, and may be needed for a buffered draw (FFX FMVs).
g_gs_renderer->Flush(GSState::VSYNC);
g_gs_renderer->VSync(field, registers_written, g_gs_renderer->IsIdleFrame());
g_gs_renderer->SubmitVsync(field, registers_written);
}
int GSfreeze(FreezeAction mode, freezeData* data)
+92
View File
@@ -3,6 +3,7 @@
#pragma once
#include "GS/GS.h"
#include "GS/GSRegs.h"
#include "GS/GSVector.h"
#include "GS/GSDrawingContext.h"
@@ -96,6 +97,97 @@ namespace GSBackQueue
u32 tail;
};
// PCRTC digest state — hoisted from GSState (GSvsync writes it once per
// frame from the privileged registers; the Draw() heuristics and the Merge
// circuit read it back-side, so it ships whole in PCRTC_SYNC records).
struct GSPCRTCRegs
{
struct PCRTCDisplay
{
bool enabled;
int FBP;
int FBW;
int PSM;
int DBY;
int DBX;
GSRegDISPFB prevFramebufferReg;
GSVector2i prevDisplayOffset;
GSVector2i displayOffset;
GSVector4i displayRect;
GSVector2i magnification;
GSVector2i prevFramebufferOffsets;
GSVector2i framebufferOffsets;
GSVector4i framebufferRect;
__fi int Block() const { return FBP << 5; }
};
int videomode = 0;
int interlaced = 0;
int FFMD = 0;
bool PCRTCSameSrc = false;
bool toggling_field = false;
PCRTCDisplay PCRTCDisplays[2] = {};
bool IsAnalogue();
// Calculates which display is closest to matching zero offsets in either direction.
GSVector2i NearestToZeroOffset();
void SetVideoMode(GSVideoMode videoModeIn);
// Enable each of the displays.
void EnableDisplays(GSRegPMODE pmode, GSRegSMODE2 smode2, bool smodetoggle);
void CheckSameSource();
bool FrameWrap();
// If the start point of both frames match, we can do a single read
bool FrameRectMatch();
GSVector2i GetResolution();
GSVector4i GetFramebufferRect(int display);
int GetFramebufferBitDepth();
GSVector2i GetFramebufferSize(int display);
// Sets up the rectangles for both the framebuffer read and the displays for the merge circuit.
void SetRects(int display, GSRegDISPLAY displayReg, GSRegDISPFB framebufferReg);
// Calculate framebuffer read offsets, should be considered if only one circuit is enabled, or difference is more than 1 line.
// Only considered if "Anti-blur" is enabled.
void CalculateFramebufferOffset(bool scanmask, GSRegDISPFB framebuffer0Reg, GSRegDISPFB framebuffer1Reg);
// Used in software mode to align the buffer when reading. Offset is accounted for (block aligned) by GetOutput.
void RemoveFramebufferOffset(int display);
// If the two displays are offset from each other, move them to the correct offsets.
// If using screen offsets, calculate the positions here.
void CalculateDisplayOffset(bool scanmask);
};
// Once-per-frame PCRTC digest, shipped BEFORE the vsync-flushed draw
// records so those draws see the fresh display state, exactly like today
// (GSvsync digests, then flushes). Mid-frame draws keep seeing the previous
// frame's digest, also like today.
struct PcrtcSyncRecord
{
GSPCRTCRegs displays;
u8 scanmask_used; // pre-decrement value; Merge's decrement stays back-side
};
// End of frame: the whole VSync() body (Merge -> present -> capture ->
// perfmon frame tick) runs back-side.
struct VsyncRecord
{
u32 field;
bool registers_written;
bool idle_frame;
};
// One flushed draw (today's FlushPrim tail: vertex trace -> texel rounding ->
// Draw() -> perfmon). Self-contained: the executor installs the env snapshots
// and scalars, then runs the tail against the referenced buffers, which it
+19
View File
@@ -3240,6 +3240,25 @@ void GSState::ExecClutLoadRecord(const GSBackQueue::ClutLoadRecord& rec)
m_mem.m_clut.WriteLoad(rec.TEX0, rec.TEXCLUT);
}
void GSState::SubmitPcrtcSync()
{
GSBackQueue::PcrtcSyncRecord rec;
std::memcpy(&rec.displays, &PCRTCDisplays, sizeof(rec.displays));
rec.scanmask_used = m_scanmask_used;
ExecPcrtcSyncRecord(rec);
}
void GSState::ExecPcrtcSyncRecord(const GSBackQueue::PcrtcSyncRecord& rec)
{
// Inline this re-writes the digest it was captured from; on the back object
// (GV7-1) it refreshes the copy read by the Draw() heuristics and the Merge
// circuit. Merge's scanmask decrement stays back-side; the front mirrors it
// at enqueue once the copies are distinct.
std::memcpy(&PCRTCDisplays, &rec.displays, sizeof(PCRTCDisplays));
m_scanmask_used = rec.scanmask_used;
}
void GSState::Move()
{
// ffxii uses this to move the top/bottom of the scrolling menus offscreen and then blends them back over the text to create a shading effect
+5 -67
View File
@@ -477,74 +477,10 @@ public:
std::vector<size_t> m_drawlist;
std::vector<GSVector4i> m_drawlist_bbox;
struct GSPCRTCRegs
{
struct PCRTCDisplay
{
bool enabled;
int FBP;
int FBW;
int PSM;
int DBY;
int DBX;
GSRegDISPFB prevFramebufferReg;
GSVector2i prevDisplayOffset;
GSVector2i displayOffset;
GSVector4i displayRect;
GSVector2i magnification;
GSVector2i prevFramebufferOffsets;
GSVector2i framebufferOffsets;
GSVector4i framebufferRect;
// Definition hoisted to GSBackQueue.h (PCRTC_SYNC record payload type).
using GSPCRTCRegs = GSBackQueue::GSPCRTCRegs;
__fi int Block() const { return FBP << 5; }
};
int videomode = 0;
int interlaced = 0;
int FFMD = 0;
bool PCRTCSameSrc = false;
bool toggling_field = false;
PCRTCDisplay PCRTCDisplays[2] = {};
bool IsAnalogue();
// Calculates which display is closest to matching zero offsets in either direction.
GSVector2i NearestToZeroOffset();
void SetVideoMode(GSVideoMode videoModeIn);
// Enable each of the displays.
void EnableDisplays(GSRegPMODE pmode, GSRegSMODE2 smode2, bool smodetoggle);
void CheckSameSource();
bool FrameWrap();
// If the start point of both frames match, we can do a single read
bool FrameRectMatch();
GSVector2i GetResolution();
GSVector4i GetFramebufferRect(int display);
int GetFramebufferBitDepth();
GSVector2i GetFramebufferSize(int display);
// Sets up the rectangles for both the framebuffer read and the displays for the merge circuit.
void SetRects(int display, GSRegDISPLAY displayReg, GSRegDISPFB framebufferReg);
// Calculate framebuffer read offsets, should be considered if only one circuit is enabled, or difference is more than 1 line.
// Only considered if "Anti-blur" is enabled.
void CalculateFramebufferOffset(bool scanmask, GSRegDISPFB framebuffer0Reg, GSRegDISPFB framebuffer1Reg);
// Used in software mode to align the buffer when reading. Offset is accounted for (block aligned) by GetOutput.
void RemoveFramebufferOffset(int display);
// If the two displays are offset from each other, move them to the correct offsets.
// If using screen offsets, calculate the positions here.
void CalculateDisplayOffset(bool scanmask);
} PCRTCDisplays;
GSPCRTCRegs PCRTCDisplays;
public:
/// Returns the appropriate directory for draw dumping.
@@ -600,6 +536,8 @@ public:
void SubmitClutLoad(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT);
void ExecClutLoadRecord(const GSBackQueue::ClutLoadRecord& rec);
void ExecDrawRecord(const GSBackQueue::DrawRecord& rec);
void SubmitPcrtcSync();
void ExecPcrtcSyncRecord(const GSBackQueue::PcrtcSyncRecord& rec);
GSVector4i GetTEX0Rect(GSDrawingContext prev_ctx);
void CheckWriteOverlap(bool req_write, bool req_read);
+15
View File
@@ -580,6 +580,21 @@ void GSRenderer::EndPresentFrame()
ImGuiManager::NewFrame();
}
void GSRenderer::SubmitVsync(u32 field, bool registers_written)
{
GSBackQueue::VsyncRecord rec;
rec.field = field;
rec.registers_written = registers_written;
rec.idle_frame = IsIdleFrame(); // front-computable: compares serials against the last frame's
ExecVsyncRecord(rec);
}
void GSRenderer::ExecVsyncRecord(const GSBackQueue::VsyncRecord& rec)
{
VSync(rec.field, rec.registers_written, rec.idle_frame);
}
void GSRenderer::VSync(u32 field, bool registers_written, bool idle_frame)
{
if (GSConfig.ShouldDump(s_n, g_perfmon.GetFrame()))
+2
View File
@@ -41,6 +41,8 @@ public:
virtual void UpdateRenderFixes();
virtual void VSync(u32 field, bool registers_written, bool idle_frame);
void SubmitVsync(u32 field, bool registers_written);
void ExecVsyncRecord(const GSBackQueue::VsyncRecord& rec);
virtual bool CanUpscale() { return false; }
virtual float GetUpscaleMultiplier() { return 1.0f; }
virtual float GetTextureScaleFactor() { return 1.0f; }