/*
Copyright 2026 flyinghead
This file is part of Flycast.
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Flycast. If not, see .
*/
#define GLM_FORCE_SWIZZLE 1
#include "transform_matrix.h"
#include
static void getTAViewport(const rend_context& rendCtx, int& width, int& height) {
width = rendCtx.globClip.x;
height = rendCtx.globClip.y;
}
static void makePowerOf2Size(int width, int height, int& wpo2, int& hpo2)
{
wpo2 = 8;
hpo2 = 8;
for (int v = 0; v < 8; v++)
{
if (wpo2 < width)
wpo2 *= 2;
if (hpo2 < height)
hpo2 *= 2;
}
}
void getPvrFramebufferSize(const rend_context& rendCtx, int& width, int& height)
{
if (!rendCtx.isRTT)
{
// Render to screen
getTAViewport(rendCtx, width, height);
if (!config::EmulateFramebuffer)
{
int maxHeight = FB_R_CTRL.vclk_div == 0 && SPG_CONTROL.interlace == 0 ? 240 : 480;
// we ignore vscalefactor when interlaced because it's used for Stretched PAL (factor in ]1, 1.3]),
// or Flicker-free interlace mode B (factor [0.5, 0.6]), which are only emulated in Full FB emu.
// In 240p, some games (Cho - Hatsumei Boy Kanipan) use a 1/2 factor from rendering at 480 to a 240-line framebuffer.
// So we render at 480 in that case.
if (rendCtx.scaler_ctl.vscalefactor != 0
&& (rendCtx.scaler_ctl.vscalefactor > 1025 || rendCtx.scaler_ctl.vscalefactor < 1024)
&& SPG_CONTROL.interlace == 0)
maxHeight /= 1024.f / rendCtx.scaler_ctl.vscalefactor;
if (FB_R_CTRL.fb_line_double)
maxHeight /= 2;
height = std::min(maxHeight, height);
}
}
else
{
// Render to texture
if (config::RenderToTextureBuffer) {
width = rendCtx.tileClip.bottomRight().x + 1;
height = rendCtx.tileClip.bottomRight().y + 1;
}
else {
Rect clip = intersect(rendCtx.tileClip, rendCtx.fbClip);
makePowerOf2Size(clip.bottomRight().x + 1, clip.bottomRight().y + 1, width, height);
}
}
}
bool TransformMatrix::isClipped() const
{
int width, height;
getTAViewport(*renderingContext, width, height);
Rect clip = intersectTileAndFBScissor();
return clip.origin.x > 0
|| clip.bottomRight().x < width - 1;
}
void TransformMatrix::CalcMatrices(const rend_context *renderingContext, int width, int height)
{
const int flipY = directx ? -1 : 1;
glm::vec2 renderViewport = { width == 0 ? settings.display.width : width, height == 0 ? settings.display.height : height };
glm::vec2 dcViewport;
this->renderingContext = renderingContext;
int w, h;
getPvrFramebufferSize(*renderingContext, w, h);
dcViewport.x = (float)w;
dcViewport.y = (float)h;
if (renderingContext->isRTT) {
normalMatrix = glm::translate(glm::vec3(-1, -flipY, 0))
* glm::scale(glm::vec3(2.0f / dcViewport.x, 2.0f / dcViewport.y * flipY, 1.f));
}
else
{
float widescreenShift;
if (config::Widescreen && !config::Rotate90 && !config::EmulateFramebuffer)
{
widescreenShift = 1.f - dcViewport.x / dcViewport.y * renderViewport.y / renderViewport.x;
if (config::SuperWidescreen)
dcViewport.x *= (float)settings.display.width / settings.display.height / 4.f * 3.f;
else
dcViewport.x *= 4.f / 3.f;
}
else {
widescreenShift = 0;
}
glm::mat4 trans = glm::translate(glm::vec3(-1 + widescreenShift, -flipY, 0));
float x_coef = 2.0f / dcViewport.x;
float y_coef = 2.0f / dcViewport.y * flipY;
normalMatrix = trans * glm::scale(glm::vec3(x_coef, y_coef, 1.f));
}
normalMatrix = glm::scale(glm::vec3(1, 1, 1 / config::ExtraDepthScale)) * normalMatrix;
glm::mat4 vp_trans = glm::translate(glm::vec3(1, flipY, 0));
vp_trans = glm::scale(glm::vec3(renderViewport.x / 2, renderViewport.y / 2 * flipY, 1.f))
* vp_trans;
viewportMatrix = vp_trans * normalMatrix;
}
Rect TransformMatrix::intersectTileAndFBScissor() const
{
// Framebuffer clipping is applied after scaling (SCALER_CTL).
// Since we want to apply it before, we need to "unscale" the clipping rectangle.
glm::ivec2 botRight = renderingContext->fbClip.origin + renderingContext->fbClip.size;
float xscale, yscale;
getScissorScaling(xscale, yscale);
Rect rect;
rect.origin = glm::max(glm::ivec2(std::round(renderingContext->fbClip.origin.x * xscale), std::round(renderingContext->fbClip.origin.y * yscale)),
renderingContext->tileClip.origin);
botRight = glm::min(glm::ivec2(std::round(botRight.x * xscale), std::round(botRight.y * yscale)),
renderingContext->tileClip.origin + renderingContext->tileClip.size);
rect.size = glm::max(botRight - rect.origin, glm::ivec2(0, 0));
return rect;
}
Rect TransformMatrix::getBaseScissor() const
{
const bool widescreen = !renderingContext->isRTT && config::Widescreen
&& !isClipped() && !config::Rotate90 && !config::EmulateFramebuffer;
if (widescreen)
{
return Rect{
glm::ivec2(0, 0),
glm::ivec2(renderingContext->framebufferWidth, renderingContext->framebufferHeight)
};
}
glm::vec4 origin;
glm::vec4 size;
if ((!renderingContext->isRTT && config::EmulateFramebuffer)
|| (renderingContext->isRTT && config::RenderToTextureBuffer))
{
// Region tile clipping only
origin = glm::vec4(renderingContext->tileClip.origin, 0, 1);
size = glm::vec4(renderingContext->tileClip.size, 0, 0);
}
else
{
Rect rect = intersectTileAndFBScissor();
origin = glm::vec4(rect.origin, 0, 1);
size = glm::vec4(rect.size, 0, 0);
}
origin = viewportMatrix * origin;
size = viewportMatrix * size;
if (size.x < 0) {
origin.x += size.x;
size.x = -size.x;
}
if (size.y < 0) {
origin.y += size.y;
size.y = -size.y;
}
return Rect{
glm::ivec2(glm::max(glm::round(origin.xy()), glm::vec2(0, 0))),
glm::ivec2(glm::max(glm::round(size.xy()), glm::vec2(0, 0)))
};
}
void TransformMatrix::getScissorScaling(float& scale_x, float& scale_y) const
{
scale_x = 1.f;
scale_y = 1.f;
if ((!renderingContext->isRTT && !config::EmulateFramebuffer)
|| (renderingContext->isRTT && !config::RenderToTextureBuffer))
{
if (renderingContext->scaler_ctl.vscalefactor < 1024 || renderingContext->scaler_ctl.vscalefactor > 1025)
scale_y *= renderingContext->scaler_ctl.vscalefactor / 1024.f;
if (renderingContext->scaler_ctl.hscale == 1)
scale_x *= 2.f;
}
}
TileClipping TransformMatrix::getTileClip(u32 val, Rect& clipRect)
{
if (!config::Clipping)
return TileClipping::Off;
u32 clipmode = val >> 28;
if (clipmode < 2)
return TileClipping::Off; //always passes
TileClipping tileClippingMode;
if (clipmode & 1)
tileClippingMode = TileClipping::Inside; //render stuff outside the region
else
tileClippingMode = TileClipping::Outside; //render stuff inside the region
float csx = (float)(val & 63);
float cex = (float)((val >> 6) & 63);
float csy = (float)((val >> 12) & 31);
float cey = (float)((val >> 17) & 31);
csx = csx * 32;
cex = (cex + 1) * 32;
csy = csy * 32;
cey = (cey + 1) * 32;
if (csx == 0 && csy == 0 && cex >= renderingContext->globClip.x && cey >= renderingContext->globClip.y)
return TileClipping::Off;
glm::vec4 clip_start(csx, csy, 0, 1);
glm::vec4 clip_end(cex, cey, 0, 1);
clip_start = viewportMatrix * clip_start;
clip_end = viewportMatrix * clip_end;
clipRect = { clip_start.xy(), clip_end.xy() - clip_start.xy() };
clipRect.size.x++;
clipRect.size.y = std::abs(clipRect.size.y) + 1;
if (tileClippingMode == TileClipping::Outside)
{
Rect base = getBaseScissor();
clipRect = intersect(clipRect, base);
}
csx = clip_start[0];
csy = clip_start[1];
cey = clip_end[1];
cex = clip_end[0];
clipRect = {
glm::ivec2(std::max(0, (int)std::round(csx)), std::max(0, (int)std::round(std::min(csy, cey)))),
glm::ivec2(std::max(0, (int)std::round(cex - csx)), std::max(0, (int)std::round(std::abs(cey - csy))))
};
return tileClippingMode;
}
void getScaledFramebufferSize(const rend_context& rendCtx, int& width, int& height)
{
getPvrFramebufferSize(rendCtx, width, height);
if (!rendCtx.isRTT)
{
// Render to screen
if (!config::EmulateFramebuffer)
{
float upscaling = config::RenderResolution / 480.f;
float w = width * upscaling;
float h = height * upscaling;
if (config::Widescreen && !config::Rotate90)
{
if (config::SuperWidescreen)
w *= (float)settings.display.width / settings.display.height / 4.f * 3.f;
else
w *= 4.f / 3.f;
}
if (!config::Rotate90)
w = std::round(w / 2.f) * 2.f;
h = std::round(h);
width = w;
height = h;
}
}
else
{
// Render to texture
if (!config::RenderToTextureBuffer)
{
if (!config::EmulateFramebuffer)
{
float upscaling = config::RenderResolution / 480.f;
float w = width * upscaling;
float h = height * upscaling;
width = std::round(w);
height = std::round(h);
}
}
else
{
// We currently ignore vert and horiz scaling so we need to increase the framebuffer size accordingly
if (rendCtx.scaler_ctl.vscalefactor < 1024 || rendCtx.scaler_ctl.vscalefactor > 1025)
height = height * 1024 / rendCtx.scaler_ctl.vscalefactor;
if (rendCtx.scaler_ctl.hscale == 1)
width /= 2;
}
}
}
float getOutputFramebufferAspectRatio()
{
float aspectRatio;
if (config::Rotate90)
{
aspectRatio = 3.f / 4.f;
}
else
{
if (config::Widescreen && !config::EmulateFramebuffer)
{
if (config::SuperWidescreen)
aspectRatio = (float)settings.display.width / settings.display.height;
else
aspectRatio = 16.f / 9.f;
}
else
{
aspectRatio = 4.f / 3.f;
}
}
if (config::StretchToFill)
return (float)settings.display.width / settings.display.height;
return aspectRatio * config::ScreenStretching / 100.f;
}
void getDCFramebufferReadSize(const FramebufferInfo& info, int& width, int& height)
{
width = (info.fb_r_size.fb_x_size + 1) * 2; // in 16-bit words
height = info.fb_r_size.fb_y_size + 1;
int modulus = (info.fb_r_size.fb_modulus - 1) * 2;
int bpp;
switch (info.fb_r_ctrl.fb_depth)
{
case fbde_0555:
case fbde_565:
bpp = 2;
break;
case fbde_888:
bpp = 3;
width = (width * 2) / 3; // in pixels
modulus = (modulus * 2) / 3; // in pixels
break;
case fbde_C888:
bpp = 4;
width /= 2; // in pixels
modulus /= 2; // in pixels
break;
default:
bpp = 2;
break;
}
if (info.spg_control.interlace && width == modulus && info.fb_r_sof2 == info.fb_r_sof1 + width * bpp)
// Typical case alternating even and odd lines -> take the whole buffer at once
height *= 2;
}
void getWindowboxDimensions(int outwidth, int outheight, float renderAR, int& dx, int& dy, bool rotate)
{
if (outwidth == 0 || outheight == 0 || renderAR == 0.f) {
dx = dy = 0;
return;
}
if (config::IntegerScale)
{
int fbh = config::RenderResolution;
if (fbh == 0) {
dx = dy = 0;
return;
}
int fbw = (int)((rotate ? 1 / renderAR : renderAR) * fbh);
if (rotate)
std::swap(fbw, fbh);
int scale = std::min(outwidth / fbw, outheight / fbh);
if (scale == 0) {
scale = std::max(fbw / outwidth, fbh / outheight) + 1;
dx = (outwidth - fbw / scale) / 2;
dy = (outheight - fbh / scale) / 2;
}
else {
dx = (outwidth - fbw * scale) / 2;
dy = (outheight - fbh * scale) / 2;
}
}
else
{
float screenAR = (float)outwidth / outheight;
if (renderAR > screenAR)
dy = (int)roundf(outheight * (1 - screenAR / renderAR) / 2.f);
else
dx = (int)roundf(outwidth * (1 - renderAR / screenAR) / 2.f);
}
}
void getVideoShift(float& x, float& y)
{
const bool vga = FB_R_CTRL.vclk_div == 1;
switch (SPG_LOAD.hcount)
{
case 857: // NTSC, VGA
x = (int)VO_STARTX.HStart - (vga ? 0xa8 : 0xa4);
break;
case 863: // PAL
x = (int)VO_STARTX.HStart - 0xae;
break;
case 851: // Naomi
case 850: // meltyb
x = (int)VO_STARTX.HStart - 0xa5; // a0 for 15 kHz
break;
default:
x = 0;
INFO_LOG(RENDERER, "unknown video mode: hcount %d", SPG_LOAD.hcount);
break;
}
switch (SPG_LOAD.vcount)
{
case 524: // NTSC, VGA
y = (int)VO_STARTY.VStart_field1 - (vga ? 0x28 : 0x12);
break;
case 262: // NTSC 240p
y = (int)VO_STARTY.VStart_field1 - 0x11;
break;
case 624: // PAL
y = (int)VO_STARTY.VStart_field1 - 0x2d;
break;
case 312: // PAL 240p
y = (int)VO_STARTY.VStart_field1 - 0x2e;
break;
case 529: // Naomi 31 kHz
case 528: // meltyb
y = (int)VO_STARTY.VStart_field1 - 0x24;
break;
case 536: // Naomi 15 kHz 480i
case 268: // Naomi 15 kHz 240p
y = (int)VO_STARTY.VStart_field1 - 0x17; // 16 for 240p
break;
default:
y = 0;
INFO_LOG(RENDERER, "unknown video mode: vcount %d", SPG_LOAD.vcount);
break;
}
if (!config::EmulateFramebuffer)
{
x *= config::RenderResolution / 480.f;
y *= config::RenderResolution / 480.f;
}
if (config::StretchToFill)
{
float aspectRatio = config::Rotate90 ? 3.f / 4.f : 4.f / 3.f;
if (!config::Rotate90 && config::Widescreen && !config::EmulateFramebuffer)
aspectRatio = config::SuperWidescreen
? (float)settings.display.width / settings.display.height
: 16.f / 9.f;
x *= ((float)settings.display.width / settings.display.height) / aspectRatio;
}
else
{
x *= config::ScreenStretching / 100.f;
}
}
void getWriteFBToVramParams(const rend_context& ctx, glm::ivec2& scaledSize, Rect& finalClip)
{
glm::vec2 scale;
scale.x = ctx.scaler_ctl.hscale == 1 ? 0.5f : 1.f;
if (ctx.scaler_ctl.vscalefactor != 0
&& ctx.scaler_ctl.vscalefactor != 1024 && ctx.scaler_ctl.vscalefactor != 1025)
scale.y = 1024.f / ctx.scaler_ctl.vscalefactor;
else
scale.y = 1;
scaledSize.x = ctx.globClip.x * scale.x;
scaledSize.y = ctx.globClip.y * scale.y;
finalClip.origin = glm::vec2(ctx.tileClip.origin) * scale;
finalClip.size = glm::vec2(ctx.tileClip.size) * scale;
finalClip = intersect(finalClip, ctx.fbClip);
}