MSAA: Only multisample full size framebuffers

Smaller framebuffers are mostly effects (blur, bloom), and MSAA changes
how those come out, like a higher resolution would. Syphon Filter:
Logan's Shadow grew a glow around its lit floor panels that the PSP
doesn't have. Effects in Call of Duty: Road to Victory and Assassin's
Creed: Bloodlines shifted too.

A framebuffer that grows to full size gets MSAA from then on. The
temporary buffer that shifts depth for offset rendering now takes the
samples of the framebuffer it shifts, since they can differ.

Over the frame dumps at 2x and 4x, the differences away from edges
compared to no MSAA drop by a fifth and 53 dumps get closer. The one
that moved away, Silent Hill: Origins' flashlight shadow, now matches
the PSP; without MSAA it shows a known replay artifact there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-18 12:37:02 +02:00
1 parent 2c19007b61
commit d4cc5d8666
2 files changed
+19 -7

No files matched your search

+14 -6
View File
@@ -1078,7 +1078,7 @@ void FramebufferManagerCommon::ShiftDepthForOffsetRendering(VirtualFramebuffer *
if (w <= 0) {
return;
}
Draw::Framebuffer *temp = GetTempFBO(TempFBO::DEPTH_SHIFT, vfb->renderWidth, vfb->renderHeight);
Draw::Framebuffer *temp = GetTempFBO(TempFBO::DEPTH_SHIFT, vfb->renderWidth, vfb->renderHeight, vfb->fbo->MultiSampleLevel());
if (!temp) {
return;
}
@@ -1984,6 +1984,7 @@ void FramebufferManagerCommon::ResizeFramebufFBO(VirtualFramebuffer *vfb, int w,
}
bool creating = old.bufferWidth == 0;
vfb->multisampled = ShouldMultisample(vfb);
if (creating) {
INFO_LOG(Log::FrameBuf, "Creating %s FBO at %08x/%08x stride=%d %dx%d (force=%d)", GeBufferFormatToString(vfb->fb_format), vfb->fb_address, vfb->z_address, vfb->fb_stride, vfb->bufferWidth, vfb->bufferHeight, (int)force);
} else {
@@ -2016,7 +2017,7 @@ void FramebufferManagerCommon::ResizeFramebufFBO(VirtualFramebuffer *vfb, int w,
gpuStats.perFrame.numFBOsCreated++;
vfb->fbo = draw_->CreateFramebuffer({ vfb->renderWidth, vfb->renderHeight, 1, GetFramebufferLayers(), msaaLevel_, true, tag });
vfb->fbo = draw_->CreateFramebuffer({ vfb->renderWidth, vfb->renderHeight, 1, GetFramebufferLayers(), vfb->multisampled ? msaaLevel_ : 0, true, tag });
if (Memory::IsVRAMAddress(vfb->fb_address) && vfb->fb_stride != 0) {
NotifyMemInfo(MemBlockFlags::ALLOC, vfb->fb_address, vfb->BufferByteSize(RASTER_COLOR), tag, len);
}
@@ -3165,6 +3166,12 @@ static const char *TempFBOReasonToString(TempFBO reason) {
return "";
}
// Only full size framebuffers get MSAA. Smaller ones are mostly effects (blur, bloom), and MSAA changes those
// (Syphon Filter: Logan's Shadow grows a glow around lit floor panels).
bool FramebufferManagerCommon::ShouldMultisample(const VirtualFramebuffer *vfb) const {
return vfb->bufferWidth >= 480 && vfb->bufferHeight >= 272;
}
Draw::Framebuffer *FramebufferManagerCommon::CopyColorSample0(VirtualFramebuffer *vfb) {
if (!vfb->fbo || vfb->fbo->MultiSampleLevel() == 0) {
return nullptr;
@@ -3186,8 +3193,11 @@ Draw::Framebuffer *FramebufferManagerCommon::CopyColorSample0(VirtualFramebuffer
return copy;
}
Draw::Framebuffer *FramebufferManagerCommon::GetTempFBO(TempFBO reason, u16 w, u16 h) {
u64 key = ((u64)reason << 48) | ((u32)w << 16) | h;
Draw::Framebuffer *FramebufferManagerCommon::GetTempFBO(TempFBO reason, u16 w, u16 h, int msaaLevel) {
if (reason != TempFBO::DEPTH_SHIFT) {
msaaLevel = 0;
}
u64 key = ((u64)msaaLevel << 56) | ((u64)reason << 48) | ((u32)w << 16) | h;
auto it = tempFBOs_.find(key);
if (it != tempFBOs_.end()) {
it->second.last_frame_used = gpuStats.totals.numFlips;
@@ -3195,8 +3205,6 @@ Draw::Framebuffer *FramebufferManagerCommon::GetTempFBO(TempFBO reason, u16 w, u
}
bool z_stencil = reason == TempFBO::STENCIL || reason == TempFBO::DEPTH_SHIFT;
// The depth shift copies to and from a framebuffer, which needs the same samples.
const int msaaLevel = reason == TempFBO::DEPTH_SHIFT ? msaaLevel_ : 0;
char name[128];
snprintf(name, sizeof(name), "tempfbo_%s_%dx%d", TempFBOReasonToString(reason), w / renderScaleFactor_, h / renderScaleFactor_);
+5 -1
View File
@@ -111,6 +111,8 @@ struct VirtualFramebuffer {
// The scale factor at which we are rendering (to achieve higher resolution).
u8 renderScaleFactor;
// Whether it gets MSAA, if it's on.
bool multisampled;
u16 usageFlags;
@@ -470,7 +472,8 @@ public:
virtual void DeviceLost();
virtual void DeviceRestore(Draw::DrawContext *draw);
Draw::Framebuffer *GetTempFBO(TempFBO reason, u16 w, u16 h);
// msaaLevel is only used for DEPTH_SHIFT, which needs the samples of the framebuffer it shifts.
Draw::Framebuffer *GetTempFBO(TempFBO reason, u16 w, u16 h, int msaaLevel = 0);
// A single-sampled copy of the first sample of each pixel of a multisampled framebuffer, or null if it isn't
// multisampled or the backend can't. For reading colors as values, which averaging the samples would corrupt.
Draw::Framebuffer *CopyColorSample0(VirtualFramebuffer *vfb);
@@ -551,6 +554,7 @@ protected:
void CopyToDepthFromOverlappingFramebuffers(VirtualFramebuffer *dest);
bool UpdateRenderSize(int msaaLevel);
bool ShouldMultisample(const VirtualFramebuffer *vfb) const;
void FlushBeforeCopy();
virtual void DecimateFBOs(); // keeping it virtual to let D3D do a little extra