mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-22 10:49:05 +02:00
Video textures were clamped to a 480x272 frame on both axes, so filtering can't pull in whatever lies next to the frame in memory. The VSH's UMD Video player converts 480 rows, into a 256 or 512-wide texture it uses as a ring that its strips wrap around: the rows past 272 smeared, and the wrapped strips were stretched. The clamp now covers the rows written at the texture, and the columns the strips written into its first row cover. An axis the frame fills keeps the game's wrap mode. A game's 480x272 frame is clamped as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
289 lines
11 KiB
C++
289 lines
11 KiB
C++
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
#include "ShaderUniforms.h"
|
|
#include "Common/System/Display.h"
|
|
#include "Common/Data/Convert/SmallDataConvert.h"
|
|
#include "Common/Math/lin/matrix4x4.h"
|
|
#include "Common/Math/math_util.h"
|
|
#include "Common/Math/CrossSIMD.h"
|
|
#include "Common/Math/lin/vec3.h"
|
|
#include "Common/TimeUtil.h"
|
|
#include "GPU/GPUState.h"
|
|
#include "GPU/Common/FramebufferManagerCommon.h"
|
|
#include "GPU/Common/GPUStateUtils.h"
|
|
#include "GPU/Common/TransformCommon.h"
|
|
#include "GPU/Math3D.h"
|
|
|
|
using namespace Lin;
|
|
|
|
void UpdateRotation(float rotMatrix[4], bool useBufferedRendering) {
|
|
if (!gstate_c.Use(GPU_USE_PRE_ROTATION) || useBufferedRendering) {
|
|
rotMatrix[0] = 1.0f;
|
|
rotMatrix[1] = 0.0f;
|
|
rotMatrix[2] = 0.0f;
|
|
rotMatrix[3] = 1.0f;
|
|
} else {
|
|
const DisplayRotation rotation = g_display.rotation;
|
|
// The others are ROTATE_90 and so on.
|
|
rotMatrix[0] = rotation == DisplayRotation::ROTATE_0 ? 1.0 : (rotation == DisplayRotation::ROTATE_180 ? -1.0 : 0.0);
|
|
rotMatrix[1] = rotation == DisplayRotation::ROTATE_90 ? 1.0 : (rotation == DisplayRotation::ROTATE_270 ? -1.0 : 0.0);
|
|
rotMatrix[2] = rotation == DisplayRotation::ROTATE_270 ? 1.0 : (rotation == DisplayRotation::ROTATE_90 ? -1.0 : 0.0);
|
|
rotMatrix[3] = rotation == DisplayRotation::ROTATE_0 ? 1.0 : (rotation == DisplayRotation::ROTATE_180 ? -1.0 : 0.0);
|
|
}
|
|
}
|
|
|
|
// The part of a video texture the frame fills: as much as was written there (see
|
|
// TextureCacheCommon::ApplyTextureFinish), or 480x272 if that isn't known. The VSH's UMD Video
|
|
// player converts 480 rows, and its strips wrap around a texture they fill the width of.
|
|
int VideoFrameWidth() {
|
|
return gstate_c.videoColumns > 0 ? gstate_c.videoColumns : 480;
|
|
}
|
|
|
|
int VideoFrameHeight() {
|
|
return gstate_c.videoRows > 0 ? gstate_c.videoRows : 272;
|
|
}
|
|
|
|
bool VideoClampsS() {
|
|
return gstate.getTextureWidth(0) > VideoFrameWidth();
|
|
}
|
|
|
|
bool VideoClampsT() {
|
|
return gstate.getTextureHeight(0) > VideoFrameHeight();
|
|
}
|
|
|
|
void CalcTexClamp(float texClamp[4], float texClampOffset[2]) {
|
|
const float invW = 1.0f / (float)gstate_c.curTextureWidth;
|
|
const float invH = 1.0f / (float)gstate_c.curTextureHeight;
|
|
int w = gstate.getTextureWidth(0);
|
|
int h = gstate.getTextureHeight(0);
|
|
if (gstate_c.textureIsVideo) {
|
|
// Only the video frame itself, whatever is in memory around it may be garbage.
|
|
w = std::min(w, VideoFrameWidth());
|
|
h = std::min(h, VideoFrameHeight());
|
|
}
|
|
|
|
// First wrap xy, then half texel xy (for clamp.)
|
|
texClamp[0] = (float)w * invW;
|
|
texClamp[1] = (float)h * invH;
|
|
texClamp[2] = invW * 0.5f;
|
|
texClamp[3] = invH * 0.5f;
|
|
texClampOffset[0] = gstate_c.curTextureXOffset * invW;
|
|
texClampOffset[1] = gstate_c.curTextureYOffset * invH;
|
|
}
|
|
|
|
void BaseUpdateUniforms(UB_VS_FS_Base *ub, uint64_t dirtyUniforms, bool useBufferedRendering, bool pixelMapped) {
|
|
if (dirtyUniforms & DIRTY_TEXENV) {
|
|
Uint8x3ToFloat3(ub->texEnvColor, gstate.texenvcolor);
|
|
}
|
|
if (dirtyUniforms & DIRTY_ALPHACOLORREF) {
|
|
ub->alphaColorRef = gstate.getColorTestRef() | ((gstate.getAlphaTestRef() & gstate.getAlphaTestMask()) << 24);
|
|
}
|
|
if (dirtyUniforms & DIRTY_ALPHACOLORMASK) {
|
|
ub->colorTestMask = gstate.getColorTestMask() | (gstate.getAlphaTestMask() << 24);
|
|
}
|
|
if (dirtyUniforms & DIRTY_FOGCOLOR) {
|
|
Uint8x3ToFloat3(ub->fogColor, gstate.fogcolor);
|
|
}
|
|
if (dirtyUniforms & DIRTY_SHADERBLEND) {
|
|
Uint8x3ToFloat3(ub->blendFixA, gstate.getFixA());
|
|
Uint8x3ToFloat3(ub->blendFixB, gstate.getFixB());
|
|
}
|
|
if (dirtyUniforms & DIRTY_TEXCLAMP) {
|
|
CalcTexClamp(ub->texClamp, ub->texClampOffset);
|
|
}
|
|
|
|
if (dirtyUniforms & DIRTY_MIPBIAS) {
|
|
float mipBias = (float)gstate.getTexLevelOffset16() * (1.0 / 16.0f);
|
|
ub->mipBias = (mipBias + 0.5f) / (float)(gstate.getTextureMaxLevel() + 1);
|
|
// For GPU_USE_SHADER_LOD_BIAS. The sampler applies the bias only in auto mode, see GetSamplingParams.
|
|
ub->texLodBias = gstate.getTexLevelMode() == GE_TEXLEVEL_MODE_AUTO ? mipBias : 0.0f;
|
|
}
|
|
|
|
if (dirtyUniforms & DIRTY_PROJMATRIX) {
|
|
CopyMatrix4x4(ub->proj, gstate.projMatrix);
|
|
ub->rotation = useBufferedRendering ? 0 : (float)g_display.rotation;
|
|
}
|
|
|
|
if (dirtyUniforms & DIRTY_FRAMEBUFFER_DIM) {
|
|
ub->xywh[0] = (float)gstate_c.curRTOffsetX;
|
|
ub->xywh[1] = (float)gstate_c.curRTOffsetY;
|
|
ub->xywh[2] = (float)(2.0 / gstate_c.curRTWidth); // intentionally do double precision here.
|
|
ub->xywh[3] = (float)(2.0 / gstate_c.curRTHeight);
|
|
|
|
ub->rotation = useBufferedRendering ? 0 : (float)g_display.rotation;
|
|
}
|
|
|
|
if (dirtyUniforms & DIRTY_RASTER_OFFSET) {
|
|
ub->rasterOffset[0] = gstate.getOffsetX();
|
|
ub->rasterOffset[1] = gstate.getOffsetY();
|
|
ub->minZmaxZ[0] = (float)gstate.getDepthRangeMin();
|
|
ub->minZmaxZ[1] = (float)gstate.getDepthRangeMax();
|
|
// test sine wave
|
|
// ub->minZmaxZ[0] = (sin(time_now_d()) * 0.5f + 0.5f) * 65536.0;
|
|
}
|
|
|
|
if (dirtyUniforms & DIRTY_VIEWPORT_UNIFORMS) {
|
|
Vec4F32 vpScale = Vec4F32::LoadF24x4(&gstate.viewportxscale);
|
|
Vec4F32 vpOffset = Vec4F32::LoadF24x4(&gstate.viewportxcenter);
|
|
vpScale.Store(ub->vpScale);
|
|
vpOffset.Store(ub->vpOffset);
|
|
ub->NaN = std::numeric_limits<float>::quiet_NaN(); // Used in the shader for range culling.
|
|
}
|
|
|
|
// Transform
|
|
if (dirtyUniforms & DIRTY_WORLDMATRIX) {
|
|
// TODO: We could change the shader to directly read these "malformed" matrices, but we'd
|
|
// be doing the matrix multiplication manually.
|
|
ConvertMatrix4x3To3x4Transposed(ub->world, gstate.worldMatrix);
|
|
}
|
|
if (dirtyUniforms & DIRTY_VIEWMATRIX) {
|
|
ConvertMatrix4x3To3x4Transposed(ub->view, gstate.viewMatrix);
|
|
}
|
|
if (dirtyUniforms & DIRTY_TEXMATRIX) {
|
|
ConvertMatrix4x3To3x4Transposed(ub->tex, gstate.tgenMatrix);
|
|
}
|
|
|
|
if (dirtyUniforms & DIRTY_FOGCOEF) {
|
|
UpdateFogCoef(gstate, ub->fogCoef);
|
|
}
|
|
|
|
if (dirtyUniforms & DIRTY_TEX_ALPHA_MUL) {
|
|
bool doTextureAlpha = gstate.isTextureAlphaUsed();
|
|
if (gstate_c.textureSolidAlpha && gstate.getTextureFunction() != GE_TEXFUNC_REPLACE) {
|
|
doTextureAlpha = false;
|
|
}
|
|
ub->texNoAlpha = doTextureAlpha ? 0.0f : 1.0f;
|
|
ub->texMul = gstate.isColorDoublingEnabled() ? 2.0f : 1.0f;
|
|
}
|
|
|
|
if (dirtyUniforms & DIRTY_STENCILREPLACEVALUE) {
|
|
ub->stencilReplaceValue = (float)gstate.getStencilTestRef() * (1.0 / 255.0);
|
|
}
|
|
|
|
// Note - this one is not in lighting but in transformCommon as it has uses beyond lighting
|
|
if (dirtyUniforms & DIRTY_MATAMBIENTALPHA) {
|
|
Uint8x3ToFloat4_AlphaUint8(ub->matAmbient, gstate.materialambient, gstate.getMaterialAmbientA());
|
|
}
|
|
|
|
if (dirtyUniforms & DIRTY_COLORWRITEMASK) {
|
|
ub->colorWriteMask = ~((gstate.pmska << 24) | (gstate.pmskc & 0xFFFFFF));
|
|
}
|
|
|
|
// Texturing
|
|
if (dirtyUniforms & DIRTY_UVSCALEOFFSET) {
|
|
UpdateUVScaleOff(gstate, ub->uvScaleOffset);
|
|
}
|
|
|
|
if (dirtyUniforms & DIRTY_DEPAL) {
|
|
ub->depal_mask_shift_off_fmt = PackDepalBits(pixelMapped);
|
|
}
|
|
}
|
|
|
|
uint32_t PackDepalBits(bool pixelMapped) {
|
|
const int indexMask = gstate.getClutIndexMask();
|
|
const int indexShift = gstate.getClutIndexShift();
|
|
const int indexOffset = gstate.getClutIndexStartPos() >> 4;
|
|
const int format = gstate_c.depalTextureFormat;
|
|
uint32_t val = BytesToUint32(indexMask, indexShift, indexOffset, format);
|
|
// NOTE: This must follow similar logic to TextureCacheCommon::GetSamplingParams -
|
|
// maybe we can share it somehow.
|
|
// TODO: Handle replaced textures.
|
|
bool bilinear = gstate.isMagnifyFilteringEnabled() && !pixelMapped;
|
|
switch (g_Config.iTexFiltering) {
|
|
case TEX_FILTER_FORCE_NEAREST:
|
|
bilinear = false;
|
|
break;
|
|
case TEX_FILTER_FORCE_LINEAR:
|
|
if (CanForceBilinear(gstate)) {
|
|
bilinear = true;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (bilinear) {
|
|
// Poke in a bilinear filter flag in the top bit.
|
|
val |= 0x80000000;
|
|
}
|
|
return val;
|
|
}
|
|
|
|
// For "light ubershader" bits.
|
|
// TODO: We pack these bits even when not using ubershader lighting. Maybe not bother.
|
|
uint32_t PackLightControlBits() {
|
|
// Bit organization
|
|
// Bottom 4 bits are enable bits for each light.
|
|
// Then, for each light, comes 2 bits for "comp" and 2 bits for "type".
|
|
// At the end, at bit 20, we put the three material update bits.
|
|
|
|
uint32_t lightControl = 0;
|
|
for (int i = 0; i < 4; i++) {
|
|
if (gstate.isLightChanEnabled(i)) {
|
|
lightControl |= 1 << i;
|
|
}
|
|
|
|
u32 computation = (u32)gstate.getLightComputation(i); // 2 bits
|
|
u32 type = (u32)gstate.getLightType(i); // 2 bits
|
|
if (type == 3) { type = 0; } // Don't want to handle this degenerate case in the shader.
|
|
lightControl |= computation << (4 + i * 4);
|
|
lightControl |= type << (4 + i * 4 + 2);
|
|
}
|
|
|
|
// Material update is 3 bits.
|
|
lightControl |= gstate.getMaterialUpdate() << 20;
|
|
return lightControl;
|
|
}
|
|
|
|
void LightUpdateUniforms(UB_VS_Lights *ub, uint64_t dirtyUniforms) {
|
|
// Lighting
|
|
if (dirtyUniforms & DIRTY_AMBIENT) {
|
|
Uint8x3ToFloat4_AlphaUint8(ub->ambientColor, gstate.ambientcolor, gstate.getAmbientA());
|
|
}
|
|
if (dirtyUniforms & DIRTY_MATDIFFUSE) {
|
|
Uint8x3ToFloat4(ub->materialDiffuse, gstate.materialdiffuse);
|
|
}
|
|
if (dirtyUniforms & DIRTY_MATSPECULAR) {
|
|
Uint8x3ToFloat4_Alpha(ub->materialSpecular, gstate.materialspecular, std::max(0.0f, PSPLightExponent(getFloat24(gstate.materialspecularcoef))));
|
|
}
|
|
if (dirtyUniforms & DIRTY_MATEMISSIVE) {
|
|
// We're not touching the fourth f32 here, because we store an u32 of control bits in it.
|
|
Uint8x3ToFloat3(ub->materialEmissive, gstate.materialemissive);
|
|
}
|
|
if (dirtyUniforms & DIRTY_LIGHT_CONTROL) {
|
|
ub->lightControl = PackLightControlBits();
|
|
}
|
|
for (int i = 0; i < 4; i++) {
|
|
if (dirtyUniforms & (DIRTY_LIGHT0 << i)) {
|
|
if (gstate.isDirectionalLight(i)) {
|
|
// Prenormalize
|
|
ExpandFloat24x3ToFloat4AndNormalize(ub->lpos[i], &gstate.lpos[i * 3]);
|
|
} else {
|
|
ExpandFloat24x3ToFloat4(ub->lpos[i], &gstate.lpos[i * 3]);
|
|
}
|
|
// ldir is only used for spotlights. Prenormalize it.
|
|
ExpandFloat24x3ToFloat4AndNormalize(ub->ldir[i], &gstate.ldir[i * 3]);
|
|
ExpandFloat24x3ToFloat4(ub->latt[i], &gstate.latt[i * 3]);
|
|
float lightAngle_spotCoef[2] = { getFloat24(gstate.lcutoff[i]), PSPLightExponent(getFloat24(gstate.lconv[i])) };
|
|
CopyFloat2To4(ub->lightAngle_SpotCoef[i], lightAngle_spotCoef);
|
|
Uint8x3ToFloat4(ub->lightAmbient[i], gstate.lcolor[i * 3]);
|
|
Uint8x3ToFloat4(ub->lightDiffuse[i], gstate.lcolor[i * 3 + 1]);
|
|
Uint8x3ToFloat4(ub->lightSpecular[i], gstate.lcolor[i * 3 + 2]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UpdateFogCoef(const GEState &state, float fogCoef[2]) {
|
|
fogCoef[0] = getFloat24(gstate.fog1);
|
|
fogCoef[1] = getFloat24(gstate.fog2);
|
|
// The PSP just ignores infnan here (ignoring IEEE), so take it down to a valid float.
|
|
// Workaround for https://github.com/hrydgard/ppsspp/issues/5384#issuecomment-38365988
|
|
if (my_isnanorinf(fogCoef[0])) {
|
|
// Not really sure what a sensible value might be, but let's try 64k.
|
|
fogCoef[0] = std::signbit(fogCoef[0]) ? -65535.0f : 65535.0f;
|
|
}
|
|
if (my_isnanorinf(fogCoef[1])) {
|
|
fogCoef[1] = std::signbit(fogCoef[1]) ? -65535.0f : 65535.0f;
|
|
}
|
|
}
|