Files
ppsspp/GPU/Common/DrawEngineCommon.cpp
T
Henrik Rydgård 169f473b95 Smart 2D texture filtering: Use the new vertex path to check for pixel mapping in transformed geometry
Previously, "Smart 2D texture filtering" was limited to through-mode
draws, but many games use fully transformed draws to do 2D elements.

An example of this is Outrun in the menus, and it's always been plagued by
horrible filtering artifacts.

With the improved Smart 2D texture filtering, the artifacts are gone!
Of course, instead of glitchy bilinear you instead get point sampled
texturing so it looks more pixellated, but no more ugly borders between letters
in the game.
2026-06-04 17:04:04 +02:00

1267 lines
43 KiB
C++

// Copyright (c) 2013- PPSSPP Project.
// This program 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, version 2.0 or later versions.
// This program 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 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include <cfloat>
#include <cmath>
#include "Common/Data/Convert/ColorConv.h"
#include "Common/Profiler/Profiler.h"
#include "Common/LogReporting.h"
#include "Common/Math/SIMDHeaders.h"
#include "Common/Math/CrossSIMD.h"
#include "Common/Math/lin/matrix4x4.h"
#include "Common/TimeUtil.h"
#include "Core/System.h"
#include "Core/Config.h"
#include "GPU/GPUCommon.h"
#include "GPU/Common/DrawEngineCommon.h"
#include "GPU/GPUStateSIMDUtil.h"
#include "GPU/Common/SplineCommon.h"
#include "GPU/Common/DepthRaster.h"
#include "GPU/Common/ShaderId.h"
#include "GPU/Common/VertexDecoderCommon.h"
#include "GPU/Common/SoftwareTransformCommon.h"
#include "GPU/ge_constants.h"
#include "GPU/GPUState.h"
enum {
TRANSFORMED_VERTEX_BUFFER_SIZE = VERTEX_BUFFER_MAX * sizeof(TransformedVertex),
};
DrawEngineCommon::DrawEngineCommon() : decoderMap_(32) {
if (g_Config.bVertexDecoderJit && (g_Config.iCpuCore == (int)CPUCore::JIT || g_Config.iCpuCore == (int)CPUCore::JIT_IR)) {
decJitCache_ = new VertexDecoderJitCache();
}
transformed_ = (TransformedVertex *)AllocateMemoryPages(TRANSFORMED_VERTEX_BUFFER_SIZE, MEM_PROT_READ | MEM_PROT_WRITE);
transformedExpanded_ = (TransformedVertex *)AllocateMemoryPages(3 * TRANSFORMED_VERTEX_BUFFER_SIZE, MEM_PROT_READ | MEM_PROT_WRITE);
decoded_ = (u8 *)AllocateMemoryPages(DECODED_VERTEX_BUFFER_SIZE, MEM_PROT_READ | MEM_PROT_WRITE);
decIndex_ = (u16 *)AllocateMemoryPages(DECODED_INDEX_BUFFER_SIZE, MEM_PROT_READ | MEM_PROT_WRITE);
_dbg_assert_(transformed_);
_dbg_assert_(transformedExpanded_);
_dbg_assert_(decoded_);
_dbg_assert_(decIndex_);
indexGen.Setup(decIndex_);
InitDepthRaster();
}
DrawEngineCommon::~DrawEngineCommon() {
FreeMemoryPages(decoded_, DECODED_VERTEX_BUFFER_SIZE);
FreeMemoryPages(decIndex_, DECODED_INDEX_BUFFER_SIZE);
FreeMemoryPages(transformed_, TRANSFORMED_VERTEX_BUFFER_SIZE);
FreeMemoryPages(transformedExpanded_, 3 * TRANSFORMED_VERTEX_BUFFER_SIZE);
ShutdownDepthRaster();
delete decJitCache_;
decoderMap_.Iterate([&](const uint32_t vtype, VertexDecoder *decoder) {
delete decoder;
});
ClearSplineBezierWeights();
}
void DrawEngineCommon::Init() {
NotifyConfigChanged();
}
std::vector<std::string> DrawEngineCommon::DebugGetVertexLoaderIDs() {
std::vector<std::string> ids;
decoderMap_.Iterate([&](const uint32_t vtype, VertexDecoder *decoder) {
std::string id;
id.resize(sizeof(vtype));
memcpy(&id[0], &vtype, sizeof(vtype));
ids.push_back(id);
});
return ids;
}
std::string DrawEngineCommon::DebugGetVertexLoaderString(std::string_view id, DebugShaderStringType stringType) {
if (id.size() < sizeof(u32)) {
return "N/A";
}
u32 mapId;
memcpy(&mapId, &id[0], sizeof(mapId));
VertexDecoder *dec;
if (decoderMap_.Get(mapId, &dec)) {
return dec->GetString(stringType);
} else {
return "N/A";
}
}
void DrawEngineCommon::NotifyConfigChanged() {
if (decJitCache_)
decJitCache_->Clear();
lastVType_ = -1;
dec_ = nullptr;
decoderMap_.Iterate([&](const uint32_t vtype, VertexDecoder *decoder) {
delete decoder;
});
decoderMap_.Clear();
useHWTransform_ = g_Config.bHardwareTransform;
useHWTessellation_ = UpdateUseHWTessellation(g_Config.bHardwareTessellation);
}
void DrawEngineCommon::DispatchSubmitImm(GEPrimitiveType prim, TransformedVertex *buffer, int vertexCount, int cullMode, bool continuation) {
// Instead of plumbing through properly (we'd need to inject these pretransformed vertices in the middle
// of SoftwareTransform(), which would take a lot of refactoring), we'll cheat and just turn these into
// through vertices.
// Since the only known use is Thrillville and it only uses it to clear, we just use color and pos.
struct ImmVertex {
float uv[2];
uint32_t color;
float xyz[3];
};
std::vector<ImmVertex> temp;
temp.resize(vertexCount);
uint32_t color1Used = 0;
for (int i = 0; i < vertexCount; i++) {
// Since we're sending through, scale back up to w/h.
temp[i].uv[0] = buffer[i].u * gstate.getTextureWidth(0);
temp[i].uv[1] = buffer[i].v * gstate.getTextureHeight(0);
temp[i].color = buffer[i].color0_32;
temp[i].xyz[0] = buffer[i].pos[0];
temp[i].xyz[1] = buffer[i].pos[1];
temp[i].xyz[2] = buffer[i].pos[2];
color1Used |= buffer[i].color1_32;
}
int vtype = GE_VTYPE_TC_FLOAT | GE_VTYPE_POS_FLOAT | GE_VTYPE_COL_8888 | GE_VTYPE_THROUGH;
// TODO: Handle fog and secondary color somehow?
if (gstate.isFogEnabled() && !gstate.isModeThrough()) {
WARN_LOG_REPORT_ONCE(geimmfog, Log::G3D, "Imm vertex used fog");
}
if (color1Used != 0 && gstate.isUsingSecondaryColor() && !gstate.isModeThrough()) {
WARN_LOG_REPORT_ONCE(geimmcolor1, Log::G3D, "Imm vertex used secondary color");
}
bool prevThrough = gstate.isModeThrough();
// Code checks this reg directly, not just the vtype ID.
if (!prevThrough) {
gstate.vertType |= GE_VTYPE_THROUGH;
gstate_c.Dirty(DIRTY_VERTEXSHADER_STATE | DIRTY_FRAGMENTSHADER_STATE | DIRTY_RASTER_STATE | DIRTY_VIEWPORTSCISSOR_STATE);
}
int bytesRead;
uint32_t vertTypeID = GetVertTypeID(vtype, 0, applySkinInDecode_);
bool clockwise = !gstate.isCullEnabled() || gstate.getCullMode() == cullMode;
VertexDecoder *dec = GetVertexDecoder(vertTypeID);
SubmitPrim(&temp[0], nullptr, prim, vertexCount, dec, vertTypeID, clockwise, &bytesRead, clipInfoFlags_);
Flush();
if (!prevThrough) {
gstate.vertType &= ~GE_VTYPE_THROUGH;
gstate_c.Dirty(DIRTY_VERTEXSHADER_STATE | DIRTY_FRAGMENTSHADER_STATE | DIRTY_RASTER_STATE | DIRTY_VIEWPORTSCISSOR_STATE);
}
}
// This code has plenty of potential for optimization.
//
// It does the simplest and safest test possible: If all points of a bbox is outside a single of
// our clipping planes, we reject the box. Tighter bounds would be desirable but would take more calculations.
// The name is a slight misnomer, because any bounding shape will work, not just boxes.
//
// Potential optimizations:
// * SIMD-ify the plane culling, and also the vertex data conversion (could even group together xxxxyyyyzzzz for example)
// * Compute min/max of the verts, and then compute a bounding sphere and check that against the planes.
// - Less accurate, but..
// - Only requires six plane evaluations then.
bool DrawEngineCommon::TestBoundingBox(const void *vdata, const void *inds, int vertexCount, const VertexDecoder *dec, u32 vertType) {
// Grab temp buffer space from large offsets in decoded_. Not exactly safe for large draws.
// Although this may lead to drawing that shouldn't happen, the viewport is more complex on VR.
// Let's always say objects are within bounds.
if (vertexCount > 1024 || gstate_c.Use(GPU_USE_VIRTUAL_REALITY)) {
return true;
}
SimpleVertex *corners = (SimpleVertex *)(decoded_ + 65536 * 12);
float *verts = (float *)(decoded_ + 65536 * 18);
// Try to skip NormalizeVertices if it's pure positions. No need to bother with a vertex decoder
// and a large vertex format.
// BBOX games:
// - Outrun 2006
// - Tekken 6 (FLOAT only)
// - Smash Court Tennis 3 (All formats)
// - Need for Speed Carbon
if ((vertType & 0xFFFFFF) == GE_VTYPE_POS_FLOAT && !inds) {
// Most games that use bbox use floating point bboxes (Outrun, Tekken 6, Smash Court Tennis 3, Need for Speed Carbon etc).
// memcpy(verts, vdata, sizeof(float) * 3 * vertexCount);
verts = (float *)vdata;
} else if ((vertType & 0xFFFFFF) == GE_VTYPE_POS_8BIT && !inds) {
const s8 *vtx = (const s8 *)vdata;
for (int i = 0; i < vertexCount * 3; i++) {
verts[i] = vtx[i] * (1.0f / 128.0f);
}
} else if ((vertType & 0xFFFFFF) == GE_VTYPE_POS_16BIT && !inds) {
const s16 *vtx = (const s16 *)vdata;
for (int i = 0; i < vertexCount * 3; i++) {
verts[i] = vtx[i] * (1.0f / 32768.0f);
}
} else {
// Simplify away indices, bones, and morph before proceeding.
u8 *temp_buffer = decoded_ + 65536 * 24;
if ((inds || (vertType & (GE_VTYPE_WEIGHT_MASK | GE_VTYPE_MORPHCOUNT_MASK)))) {
// Need for Speed Carbon ends up on this path! With a single bone weight.
u16 indexLowerBound = 0;
u16 indexUpperBound = (u16)vertexCount - 1;
if (vertexCount > 0 && inds) {
GetIndexBounds(inds, vertexCount, vertType, &indexLowerBound, &indexUpperBound);
}
// TODO: Avoid normalization if just plain skinning.
// Force software skinning.
const u32 vertTypeID = GetVertTypeID(vertType, gstate.getUVGenMode(), true);
::NormalizeVertices(corners, temp_buffer, (const u8 *)vdata, indexLowerBound, indexUpperBound, dec, vertType);
IndexConverter conv(vertType, inds);
for (int i = 0; i < vertexCount; i++) {
verts[i * 3] = corners[conv(i)].pos.x;
verts[i * 3 + 1] = corners[conv(i)].pos.y;
verts[i * 3 + 2] = corners[conv(i)].pos.z;
}
} else {
// Simple, most common case.
int stride = dec->VertexSize();
int offset = dec->posoff;
switch (vertType & GE_VTYPE_POS_MASK) {
case GE_VTYPE_POS_8BIT:
for (int i = 0; i < vertexCount; i++) {
const s8 *data = (const s8 *)vdata + i * stride + offset;
for (int j = 0; j < 3; j++) {
verts[i * 3 + j] = data[j] * (1.0f / 128.0f);
}
}
break;
case GE_VTYPE_POS_16BIT:
for (int i = 0; i < vertexCount; i++) {
const s16 *data = ((const s16 *)((const s8 *)vdata + i * stride + offset));
for (int j = 0; j < 3; j++) {
verts[i * 3 + j] = data[j] * (1.0f / 32768.0f);
}
}
break;
case GE_VTYPE_POS_FLOAT:
for (int i = 0; i < vertexCount; i++)
memcpy(&verts[i * 3], (const u8 *)vdata + stride * i + offset, sizeof(float) * 3);
break;
}
}
}
// Unclear why the top/left is off by a pixel.
const int left = gstate.getOffsetX() + std::max(gstate.getRegionX1(), gstate.getScissorX1()) - 1;
const int top = gstate.getOffsetY() + std::max(gstate.getRegionY1(), gstate.getScissorY1()) - 1;
const int right = gstate.getOffsetX() + std::min(gstate.getRegionX2(), gstate.getScissorX2()) + 1;
const int bottom = gstate.getOffsetY() + std::min(gstate.getRegionY2(), gstate.getScissorY2()) + 1;
// This is strange, it seems if the draw box is at all outside the 4096x4096 coordinate space, all checks pass.
// It seems very odd that the hardware would have checks for this.
if (right >= 4096 || bottom >= 4096 || left < 1.0f || top < 1.0f) {
return true;
}
// TODO: How accurate should we be?
// TODO: Use CrossSIMD.
int insideCount[6] = {0};
for (int i = 0; i < vertexCount; i++) {
// Complete the transform to see if the vertex should be ignored. Not sure if we need to go to these lengths...
const float *objpos = verts + i * 3;
float projpos[4];
Vec3ByMatrix44(projpos, objpos, gstate_c.worldviewproj);
if (projpos[2] >= -projpos[3]) {
insideCount[4]++;
}
if (projpos[2] <= projpos[3]) {
insideCount[5]++;
}
const float w = projpos[3];
// const float invW = 1.0f / w;
const float screenpos[3] = {
(projpos[0] * gstate.getViewportXScale()) + gstate.getViewportXCenter() * w,
(projpos[1] * gstate.getViewportYScale()) + gstate.getViewportYCenter() * w,
(projpos[2] * gstate.getViewportZScale()) + gstate.getViewportZCenter() * w,
};
const float drawX = screenpos[0];
const float drawY = screenpos[1];
if (drawX >= left * w) {
insideCount[0]++;
}
if (drawX <= right * w) {
insideCount[1]++;
}
if (drawY >= top * w) {
insideCount[2]++;
}
if (drawY <= bottom * w) {
insideCount[3]++;
}
}
int countToCheck = gstate.isDepthClipEnabled() ? 6 : 4;
#if 0
// For debugging, the exclusive check. This should make it obvious where the culling borders are in screen space.
for (int i = 0; i < countToCheck; i++) {
if (insideCount[i] != vertexCount) {
return false;
}
}
#endif
for (int i = 0; i < countToCheck; i++) {
if (insideCount[i] == 0) {
// All verts were outside one side.
return false;
}
}
return true;
}
// This optionally culls collections of points against the six planes, and always computes the min and max of Z and W.
//
// The result of that is then used to determine if we need to drop down to software transform+clip or we can hand
// off to hardware, with whatever capabilities are available.
//
// NOTE: This doesn't handle through-mode or indexing (morph or skinning can be handled if they're implemented in software during decode).
template<u32 posFmt, u32 idxFmt>
static bool TestBoundingBoxFast(const float *cullMatrix, const void *vdata, const void *idata, int vertexCount, const VertexDecoder *dec, ClipInfoFlags *clipInfoFlags) {
Mat4F32 cullMat(cullMatrix);
alignas(16) static const float planesXYData[4] = { 1, -1, 1, -1 };
Vec4F32 planesXY = Vec4F32::LoadAligned(planesXYData);
Vec4S32 insideMaskXY = Vec4S32::Zero();
Vec4S32 insideMaskZ = Vec4S32::Zero(); // Note: This does some duplicate computation. We could avoid it on ARM32 using Vec2S32 but not really worth it.
Vec4S32 anyOutsideMaskZ = Vec4S32::Zero();
// Used to reduce the Z precision. This effectively implements the small offsets where Z can be very slightly outside -1..1.
// In reality we should probably affect X and Y too, but meh.
alignas(16) static const u32 vertexMaskData[4] = {0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFF00, 0xFFFFFFFF};
const int stride = dec->VertexSize();
const s8 *srcdata = (const s8 *)vdata + dec->posoff;
const s8 *data = srcdata;
const float vpZScale = gstate.getViewportZScale();
float minProjZ = FLT_MAX;
float maxProjZ = -FLT_MAX;
for (int i = 0; i < vertexCount; i++) {
switch (idxFmt) {
case GE_VTYPE_IDX_8BIT:
{
u8 idx = ((u8 *)idata)[i];
data = (const s8 *)srcdata + idx * stride;
break;
}
case GE_VTYPE_IDX_16BIT:
{
u16 idx = ((u16 *)idata)[i];
data = (const s8 *)srcdata + idx * stride;
break;
}
case GE_VTYPE_IDX_32BIT:
{
u32 idx = ((u32 *)idata)[i];
data = (const s8 *)srcdata + idx * stride;
break;
}
}
Vec4F32 objPos;
switch (posFmt) {
case GE_VTYPE_POS_8BIT:
objPos = Vec4F32::LoadS8Norm(data);
break;
case GE_VTYPE_POS_16BIT:
objPos = Vec4F32::LoadS16Norm((const s16 *)data);
break;
default:
objPos = Vec4F32::Load((const float *)data);
break;
}
Vec4F32 clipPos = objPos.AsVec3ByMatrix44(cullMat);
Vec4F32 posW = clipPos.ShuffleWWWW();
Vec4F32 posXY = clipPos.ShuffleXXYY();
Vec4F32 planeDistXY = posXY * planesXY + posW;
insideMaskXY |= planeDistXY.CompareGe(Vec4F32::Zero());
Vec4F32 posZ = clipPos.ShuffleZZZZ(); // This means that we compute the Z sides twice. Oh well.
Vec4F32 planeDistZ = posZ * planesXY + posW;
anyOutsideMaskZ |= planeDistZ.CompareLt(Vec4F32::Zero());
insideMaskZ |= planeDistZ.CompareGe(Vec4F32::Zero());
const float projZ = vpZScale * clipPos[2] / clipPos[3];
if (projZ < minProjZ) {
minProjZ = projZ;
}
if (projZ > maxProjZ) { // else ruins the minss/maxss optimization.
maxProjZ = projZ;
}
if (idxFmt == GE_VTYPE_IDX_NONE) {
data += stride;
}
}
if (!AllCompareBitsSet(insideMaskXY) || !AllCompareBitsSet(insideMaskZ)) {
// All vertices were outside one side of the clipping cube. We can skip the draw entirely.
return false;
}
const float vpZOffset = gstate.getViewportZCenter();
minProjZ += vpZOffset;
maxProjZ += vpZOffset;
ClipInfoFlags flags = ClipInfoFlags::Valid;
// If the W=-Z plane was intersected, here we can go through the vertices again, and check for X/Y bounds for range culling.
// However! We need to find a valid way to do so by "backprojecting" the range culling into clip space, which may be a little tricky.
//
// If nothing is outside the box, the "inversion" cases (vertices hit the boundary after clipping like Flatout, Sengoku Cannon)
// cannot happen, and soft clipping is only needed if the viewport is smaller than the valid Z range.
//
// Alternatively, we just do a compat flag for the affected games until we can solve this.
if (needFragmentMinMaxClipping() && (minProjZ < gstate.getDepthRangeMin() || maxProjZ > gstate.getDepthRangeMax())) {
if (gstate_c.Use(GPU_USE_CLIP_DISTANCE)) {
flags |= ClipInfoFlags::MinMaxZClip;
} else {
// Implement min/max in the fragment shader.
flags |= ClipInfoFlags::MinMaxZDiscard;
}
}
if (AnyCompareBitsSet(anyOutsideMaskZ)) {
// Some vertices were outside the Z clipping planes. Clip againt Z=-W in software (and do culling, too).
// TODO: With a compat flag for Flatout/Sengoku, we'll be able to avoid this in many cases, unless
// GPU_USE_CULL_DISTANCE is missing, in which case we need it for culling.
flags |= ClipInfoFlags::SoftClipCull;
}
if (minProjZ == maxProjZ) {
// Probably a 2D draw. Send it through software transform!
flags |= ClipInfoFlags::FlatZ | ClipInfoFlags::SoftClipCull;
}
if (needFragmentDepthClamp() && (minProjZ < 0 || maxProjZ > 65535)) {
if (gstate_c.Use(GPU_USE_DEPTH_CLAMP)) {
flags |= ClipInfoFlags::DepthClamp;
} else {
flags |= ClipInfoFlags::DepthClampFragment;
}
}
*clipInfoFlags = flags;
return true;
}
bool DrawEngineCommon::TestBoundingBoxFast(const float *cullMatrix, const void *vdata, const void *idata, int vertexCount, const VertexDecoder *dec, u32 vertType, ClipInfoFlags *flags) {
// Although this may lead to drawing that shouldn't happen, the viewport is more complex on VR.
// Let's always say objects are within bounds.
if (gstate_c.Use(GPU_USE_VIRTUAL_REALITY)) {
return true;
} else if (vertexCount == 0) {
return false;
}
// Dispatching like this is a bit ugly, but we want to avoid every possible overhead *inside* TestBoundingBoxFast.
// That said, I'm not 100% sure it's worth it..
switch (vertType & GE_VTYPE_IDX_MASK) {
case GE_VTYPE_IDX_NONE:
switch (vertType & GE_VTYPE_POS_MASK) {
case GE_VTYPE_POS_8BIT: return ::TestBoundingBoxFast<GE_VTYPE_POS_8BIT, GE_VTYPE_IDX_NONE>(cullMatrix, vdata, nullptr, vertexCount, dec, flags);
case GE_VTYPE_POS_16BIT: return ::TestBoundingBoxFast<GE_VTYPE_POS_16BIT, GE_VTYPE_IDX_NONE>(cullMatrix, vdata, nullptr, vertexCount, dec, flags);
case GE_VTYPE_POS_FLOAT: return ::TestBoundingBoxFast<GE_VTYPE_POS_FLOAT, GE_VTYPE_IDX_NONE>(cullMatrix, vdata, nullptr, vertexCount, dec, flags);
default:
break;
}
break;
case GE_VTYPE_IDX_8BIT:
switch (vertType & GE_VTYPE_POS_MASK) {
case GE_VTYPE_POS_8BIT: return ::TestBoundingBoxFast<GE_VTYPE_POS_8BIT, GE_VTYPE_IDX_8BIT>(cullMatrix, vdata, idata, vertexCount, dec, flags);
case GE_VTYPE_POS_16BIT: return ::TestBoundingBoxFast<GE_VTYPE_POS_16BIT, GE_VTYPE_IDX_8BIT>(cullMatrix, vdata, idata, vertexCount, dec, flags);
case GE_VTYPE_POS_FLOAT: return ::TestBoundingBoxFast<GE_VTYPE_POS_FLOAT, GE_VTYPE_IDX_8BIT>(cullMatrix, vdata, idata, vertexCount, dec, flags);
default:
break;
}
break;
case GE_VTYPE_IDX_16BIT:
switch (vertType & GE_VTYPE_POS_MASK) {
case GE_VTYPE_POS_8BIT: return ::TestBoundingBoxFast<GE_VTYPE_POS_8BIT, GE_VTYPE_IDX_16BIT>(cullMatrix, vdata, idata, vertexCount, dec, flags);
case GE_VTYPE_POS_16BIT: return ::TestBoundingBoxFast<GE_VTYPE_POS_16BIT, GE_VTYPE_IDX_16BIT>(cullMatrix, vdata, idata, vertexCount, dec, flags);
case GE_VTYPE_POS_FLOAT: return ::TestBoundingBoxFast<GE_VTYPE_POS_FLOAT, GE_VTYPE_IDX_16BIT>(cullMatrix, vdata, idata, vertexCount, dec, flags);
default:
break;
}
break;
case GE_VTYPE_IDX_32BIT:
switch (vertType & GE_VTYPE_POS_MASK) {
case GE_VTYPE_POS_8BIT: return ::TestBoundingBoxFast<GE_VTYPE_POS_8BIT, GE_VTYPE_IDX_32BIT>(cullMatrix, vdata, idata, vertexCount, dec, flags);
case GE_VTYPE_POS_16BIT: return ::TestBoundingBoxFast<GE_VTYPE_POS_16BIT, GE_VTYPE_IDX_32BIT>(cullMatrix, vdata, idata, vertexCount, dec, flags);
case GE_VTYPE_POS_FLOAT: return ::TestBoundingBoxFast<GE_VTYPE_POS_FLOAT, GE_VTYPE_IDX_32BIT>(cullMatrix, vdata, idata, vertexCount, dec, flags);
default:
break;
}
break;
default:
break;
}
_dbg_assert_(false);
return true;
}
// 2D bounding box test against scissor. No indexing yet.
// Only supports non-indexed draws with float positions. TODO: Add more float formats.
bool DrawEngineCommon::TestBoundingBoxThrough(const void *vdata, int vertexCount, const VertexDecoder *dec, u32 vertType, int *bytesRead) {
// Grab temp buffer space from large offsets in decoded_. Not exactly safe for large draws.
if (vertexCount > 16) {
return true;
}
// Although this may lead to drawing that shouldn't happen, the viewport is more complex on VR.
// Let's always say objects are within bounds.
if (gstate_c.Use(GPU_USE_VIRTUAL_REALITY))
return true;
const int stride = dec->VertexSize();
const int posOffset = dec->posoff;
*bytesRead = stride * vertexCount;
bool allOutsideLeft = true;
bool allOutsideTop = true;
bool allOutsideRight = true;
bool allOutsideBottom = true;
const float left = gstate.getScissorX1();
const float top = gstate.getScissorY1();
const float right = gstate.getScissorX2() + 1;
const float bottom = gstate.getScissorY2() + 1;
switch (vertType & GE_VTYPE_POS_MASK) {
case GE_VTYPE_POS_FLOAT:
{
// TODO: This can be SIMD'd, with some trickery.
for (int i = 0; i < vertexCount; i++) {
const float *pos = (const float*)((const u8 *)vdata + stride * i + posOffset);
const float x = pos[0];
const float y = pos[1];
if (x >= left) {
allOutsideLeft = false;
}
if (x <= right) {
allOutsideRight = false;
}
if (y >= top) {
allOutsideTop = false;
}
if (y <= bottom) {
allOutsideBottom = false;
}
}
if (allOutsideLeft || allOutsideTop || allOutsideRight || allOutsideBottom) {
return false;
}
return true;
}
default:
// Shouldn't end up here with the checks outside this function.
_dbg_assert_(false);
return true;
}
}
bool DrawEngineCommon::EstimateThroughPrimSafeSize(const void *verts, const void *inds, GEPrimitiveType prim, int vertexCount, const VertexDecoder *dec, u32 vertType, int *safeWidth, int *safeHeight) {
if (prim != GE_PRIM_RECTANGLES && prim != GE_PRIM_TRIANGLES) {
return false;
}
if ((vertType & GE_VTYPE_THROUGH_MASK) == 0 || (vertType & (GE_VTYPE_WEIGHT_MASK | GE_VTYPE_MORPHCOUNT_MASK)) != 0) {
return false;
}
const int stride = dec->VertexSize();
const int posOffset = dec->posoff;
IndexConverter conv(vertType, inds);
float minX = FLT_MAX;
float minY = FLT_MAX;
float maxX = -FLT_MAX;
float maxY = -FLT_MAX;
for (int i = 0; i < vertexCount; ++i) {
const u8 *posPtr = (const u8 *)verts + conv(i) * stride + posOffset;
float x;
float y;
switch (vertType & GE_VTYPE_POS_MASK) {
case GE_VTYPE_POS_8BIT:
x = 0.0f;
y = 0.0f;
break;
case GE_VTYPE_POS_16BIT:
{
const s16_le *pos = (const s16_le *)posPtr;
x = (float)pos[0];
y = (float)pos[1];
break;
}
case GE_VTYPE_POS_FLOAT:
{
const float_le *pos = (const float_le *)posPtr;
x = pos[0];
y = pos[1];
break;
}
default:
return false;
}
minX = std::min(minX, x);
minY = std::min(minY, y);
maxX = std::max(maxX, x);
maxY = std::max(maxY, y);
}
const int scissorX1 = gstate.getScissorX1();
const int scissorY1 = gstate.getScissorY1();
const int scissorX2 = gstate.getScissorX2() + 1;
const int scissorY2 = gstate.getScissorY2() + 1;
if (maxX <= scissorX1 || maxY <= scissorY1 || minX >= scissorX2 || minY >= scissorY2) {
return false;
}
*safeWidth = std::clamp((int)ceilf(maxX), 0, scissorX2);
*safeHeight = std::clamp((int)ceilf(maxY), 0, scissorY2);
return *safeWidth > 0 && *safeHeight > 0;
}
void DrawEngineCommon::ApplyFramebufferRead(FBOTexState *fboTexState) {
if (gstate_c.Use(GPU_USE_FRAMEBUFFER_FETCH)) {
*fboTexState = FBO_TEX_READ_FRAMEBUFFER;
} else {
gpuStats.perFrame.numCopiesForShaderBlend++;
*fboTexState = FBO_TEX_COPY_BIND_TEX;
}
gstate_c.Dirty(DIRTY_SHADERBLEND);
}
int DrawEngineCommon::ComputeNumVertsToDecode() const {
int sum = 0;
for (int i = 0; i < numDrawVerts_; i++) {
sum += drawVerts_[i].indexUpperBound + 1 - drawVerts_[i].indexLowerBound;
}
return sum;
}
// Takes a list of consecutive PRIM opcodes, and extends the current draw call to include them.
// This is just a performance optimization. NOTE: This isn't compatible with really accurate culling,
// unless we refactor things a bit.
int DrawEngineCommon::ExtendNonIndexedPrim(const uint32_t *cmd, const uint32_t *stall, const VertexDecoder *dec, u32 vertTypeID, bool clockwise, int *bytesRead, bool isTriangle, ClipInfoFlags clipInfoFlags) {
if (clipInfoFlags & ClipInfoFlags::Valid) {
clipInfoFlags_ |= clipInfoFlags;
}
const uint32_t *start = cmd;
int prevDrawVerts = numDrawVerts_ - 1;
DeferredVerts &dv = drawVerts_[prevDrawVerts];
int offset = dv.vertexCount;
_dbg_assert_(numDrawInds_ <= MAX_DEFERRED_DRAW_INDS); // if it's equal, the check below will take care of it before any action is taken.
_dbg_assert_(numDrawVerts_ > 0);
if (!clockwise) {
anyCCWOrIndexed_ = true;
}
int seenPrims = 0;
int numDrawInds = numDrawInds_;
while (cmd != stall) {
uint32_t data = *cmd;
if ((data & 0xFFF80000) != 0x04000000) {
break;
}
GEPrimitiveType newPrim = static_cast<GEPrimitiveType>((data >> 16) & 7);
if (IsTrianglePrim(newPrim) != isTriangle)
break;
int vertexCount = data & 0xFFFF;
if (numDrawInds >= MAX_DEFERRED_DRAW_INDS || vertexCountInDrawCalls_ + offset + vertexCount > VERTEX_BUFFER_MAX) {
break;
}
DeferredInds &di = drawInds_[numDrawInds++];
di.indexType = 0;
di.prim = newPrim;
seenPrims |= (1 << newPrim);
di.clockwise = clockwise;
di.vertexCount = vertexCount;
di.vertDecodeIndex = prevDrawVerts;
di.offset = offset;
offset += vertexCount;
cmd++;
}
numDrawInds_ = numDrawInds;
seenPrims_ |= seenPrims;
int totalCount = offset - dv.vertexCount;
dv.vertexCount = offset;
dv.indexUpperBound = dv.vertexCount - 1;
vertexCountInDrawCalls_ += totalCount;
*bytesRead = totalCount * dec->VertexSize();
return cmd - start;
}
void DrawEngineCommon::SkipPrim(GEPrimitiveType prim, int vertexCount, const VertexDecoder *dec, int *bytesRead) {
if (!indexGen.PrimCompatible(prevPrim_, prim)) {
Flush();
}
// This isn't exactly right, if we flushed, since prims can straddle previous calls.
// But it generally works for common usage.
if (prim == GE_PRIM_KEEP_PREVIOUS) {
// Has to be set to something, let's assume POINTS (0) if no previous.
if (prevPrim_ == GE_PRIM_INVALID)
prevPrim_ = GE_PRIM_POINTS;
prim = prevPrim_;
} else {
prevPrim_ = prim;
}
*bytesRead = vertexCount * dec->VertexSize();
}
// vertTypeID is the vertex type but with the UVGen mode smashed into the top bits.
bool DrawEngineCommon::SubmitPrim(const void *verts, const void *inds, GEPrimitiveType prim, int vertexCount, const VertexDecoder *dec, u32 vertTypeID, bool clockwise, int *bytesRead, ClipInfoFlags clipInfoFlags) {
if (!indexGen.PrimCompatible(prevPrim_, prim) || numDrawVerts_ >= MAX_DEFERRED_DRAW_VERTS || numDrawInds_ >= MAX_DEFERRED_DRAW_INDS || vertexCountInDrawCalls_ + vertexCount > VERTEX_BUFFER_MAX) {
Flush();
}
if (clipInfoFlags & ClipInfoFlags::Valid) {
if (clipInfoFlags_ != clipInfoFlags) {
Flush();
}
clipInfoFlags_ = clipInfoFlags;
}
_dbg_assert_(numDrawVerts_ < MAX_DEFERRED_DRAW_VERTS);
_dbg_assert_(numDrawInds_ < MAX_DEFERRED_DRAW_INDS);
// This isn't exactly right, if we flushed, since prims can straddle previous calls.
// But it generally works for common usage.
if (prim == GE_PRIM_KEEP_PREVIOUS) {
// Has to be set to something, let's assume POINTS (0) if no previous.
if (prevPrim_ == GE_PRIM_INVALID)
prevPrim_ = GE_PRIM_POINTS;
prim = prevPrim_;
} else {
prevPrim_ = prim;
}
// If vtype has changed, setup the vertex decoder. Don't need to nullcheck dec_ since we set lastVType_ to an invalid value whenever we null it.
if (vertTypeID != lastVType_) {
dec_ = dec;
_dbg_assert_(dec->VertexType() == vertTypeID);
lastVType_ = vertTypeID;
} else {
_dbg_assert_(dec_->VertexType() == lastVType_);
}
*bytesRead = vertexCount * dec_->VertexSize();
// Check that we have enough vertices to form the requested primitive.
if (vertexCount < 3) {
if ((vertexCount < 2 && prim > 0) || (prim > GE_PRIM_LINE_STRIP && prim != GE_PRIM_RECTANGLES)) {
return false;
}
if (vertexCount <= 0) {
// Unfortunately we need to do this check somewhere since GetIndexBounds doesn't handle zero-length arrays.
return false;
}
} else if (prim == GE_PRIM_TRIANGLES) {
// Make sure the vertex count is divisible by 3, round down. See issue #7503
const int rem = vertexCount % 3;
if (rem != 0) {
vertexCount -= rem;
}
}
bool applySkin = dec_->skinInDecode;
DeferredInds &di = drawInds_[numDrawInds_++];
_dbg_assert_(numDrawInds_ <= MAX_DEFERRED_DRAW_INDS);
di.inds = inds;
int indexType = (vertTypeID & GE_VTYPE_IDX_MASK) >> GE_VTYPE_IDX_SHIFT;
if (indexType) {
anyCCWOrIndexed_ = true;
}
di.indexType = indexType;
di.prim = prim;
di.clockwise = clockwise;
if (!clockwise) {
anyCCWOrIndexed_ = true;
}
di.vertexCount = vertexCount;
const int numDrawVerts = numDrawVerts_;
di.vertDecodeIndex = numDrawVerts;
di.offset = 0;
_dbg_assert_(numDrawVerts <= MAX_DEFERRED_DRAW_VERTS);
if (inds && numDrawVerts > decodeVertsCounter_ && drawVerts_[numDrawVerts - 1].verts == verts && !applySkin) {
// Same vertex pointer as a previous un-decoded draw call - let's just extend the decode!
di.vertDecodeIndex = numDrawVerts - 1;
u16 lb;
u16 ub;
GetIndexBounds(inds, vertexCount, vertTypeID, &lb, &ub);
DeferredVerts &dv = drawVerts_[numDrawVerts - 1];
if (lb < dv.indexLowerBound)
dv.indexLowerBound = lb;
if (ub > dv.indexUpperBound)
dv.indexUpperBound = ub;
} else {
// Record a new draw, and a new index gen.
DeferredVerts &dv = drawVerts_[numDrawVerts];
numDrawVerts_ = numDrawVerts + 1; // Increment the uncached variable
dv.verts = verts;
dv.vertexCount = vertexCount;
dv.uvScale = gstate_c.uv;
// Does handle the unindexed case.
GetIndexBounds(inds, vertexCount, vertTypeID, &dv.indexLowerBound, &dv.indexUpperBound);
}
vertexCountInDrawCalls_ += vertexCount;
seenPrims_ |= (1 << prim);
if (prim == GE_PRIM_RECTANGLES && (gstate.getTextureAddress(0) & 0x3FFFFFFF) == (gstate.getFrameBufAddress() & 0x3FFFFFFF)) {
// This prevents issues with consecutive self-renders in Ridge Racer.
gstate_c.Dirty(DIRTY_TEXTURE_PARAMS);
Flush();
}
return true;
}
void DrawEngineCommon::BeginFrame() {
applySkinInDecode_ = g_Config.bSoftwareSkinning;
}
void DrawEngineCommon::DecodeVerts(const VertexDecoder *dec, u8 *dest) {
const int numDrawVerts = numDrawVerts_;
if (!numDrawVerts) {
return;
}
// Note that this should be able to continue a partial decode - we don't necessarily start from zero here (although we do most of the time).
int i = decodeVertsCounter_;
const int stride = (int)dec->GetDecVtxFmt().stride;
int numDecodedVerts = numDecodedVerts_; // Move to a local for better codegen.
for (; i < numDrawVerts; i++) {
const DeferredVerts &dv = drawVerts_[i];
const int indexLowerBound = dv.indexLowerBound;
drawVertexOffsets_[i] = numDecodedVerts - indexLowerBound;
const int indexUpperBound = dv.indexUpperBound;
const int count = indexUpperBound - indexLowerBound + 1;
if (count + numDecodedVerts >= VERTEX_BUFFER_MAX) {
// Hit our limit! Stop decoding in this draw.
break;
}
// Decode the verts (and at the same time apply morphing/skinning). Simple.
const u8 *startPos = (const u8 *)dv.verts + indexLowerBound * dec->VertexSize();
dec->DecodeVerts(dest + numDecodedVerts * stride, startPos, &dv.uvScale, count);
numDecodedVerts += count;
}
numDecodedVerts_ = numDecodedVerts;
decodeVertsCounter_ = i;
}
int DrawEngineCommon::DecodeInds() {
// Note that this should be able to continue a partial decode - we don't necessarily start from zero here (although we do most of the time).
int i = decodeIndsCounter_;
for (; i < numDrawInds_; i++) {
const DeferredInds &di = drawInds_[i];
const int indexOffset = drawVertexOffsets_[di.vertDecodeIndex] + di.offset;
const bool clockwise = di.clockwise;
// We've already collapsed subsequent draws with the same vertex pointer, so no tricky logic here anymore.
// 2. Loop through the drawcalls, translating indices as we go.
switch (di.indexType) {
case GE_VTYPE_IDX_NONE >> GE_VTYPE_IDX_SHIFT:
indexGen.AddPrim(di.prim, di.vertexCount, indexOffset, clockwise);
break;
case GE_VTYPE_IDX_8BIT >> GE_VTYPE_IDX_SHIFT:
indexGen.TranslatePrim(di.prim, di.vertexCount, (const u8 *)di.inds, indexOffset, clockwise);
break;
case GE_VTYPE_IDX_16BIT >> GE_VTYPE_IDX_SHIFT:
indexGen.TranslatePrim(di.prim, di.vertexCount, (const u16_le *)di.inds, indexOffset, clockwise);
break;
case GE_VTYPE_IDX_32BIT >> GE_VTYPE_IDX_SHIFT:
indexGen.TranslatePrim(di.prim, di.vertexCount, (const u32_le *)di.inds, indexOffset, clockwise);
break;
}
}
decodeIndsCounter_ = i;
return indexGen.VertexCount();
}
bool DrawEngineCommon::CanUseHardwareTransform(int prim) const {
if (!useHWTransform_)
return false;
return !gstate.isModeThrough() && prim != GE_PRIM_RECTANGLES && prim > GE_PRIM_LINE_STRIP;
}
bool DrawEngineCommon::CanUseHardwareTessellation(GEPatchPrimType prim) const {
if (useHWTessellation_) {
return CanUseHardwareTransform(PatchPrimToPrim(prim));
}
return false;
}
void TessellationDataTransfer::CopyControlPoints(float *pos, float *tex, float *col, int posStride, int texStride, int colStride, const SimpleVertex *const *points, int size, u32 vertType) {
bool hasColor = (vertType & GE_VTYPE_COL_MASK) != 0;
bool hasTexCoord = (vertType & GE_VTYPE_TC_MASK) != 0;
for (int i = 0; i < size; ++i) {
memcpy(pos, points[i]->pos.AsArray(), 3 * sizeof(float));
pos += posStride;
}
if (hasTexCoord) {
for (int i = 0; i < size; ++i) {
memcpy(tex, points[i]->uv, 2 * sizeof(float));
tex += texStride;
}
}
if (hasColor) {
for (int i = 0; i < size; ++i) {
memcpy(col, Vec4f::FromRGBA(points[i]->color_32).AsArray(), 4 * sizeof(float));
col += colStride;
}
}
}
bool DrawEngineCommon::DescribeCodePtr(const u8 *ptr, std::string &name) const {
if (!decJitCache_ || !decJitCache_->IsInSpace(ptr)) {
return false;
}
// Loop through all the decoders and see if we have a match.
VertexDecoder *found = nullptr;
u32 foundKey;
decoderMap_.Iterate([&](u32 key, VertexDecoder *value) {
if (!found) {
if (value->IsInSpace(ptr)) {
foundKey = key;
found = value;
}
}
});
if (found) {
char temp[256];
found->ToString(temp, false);
name = temp;
snprintf(temp, sizeof(temp), "_%08X", foundKey);
name += temp;
return true;
} else {
return false;
}
}
enum {
DEPTH_TRANSFORMED_MAX_VERTS = VERTEX_BUFFER_MAX,
DEPTH_TRANSFORMED_BYTES = DEPTH_TRANSFORMED_MAX_VERTS * 4 * sizeof(float),
DEPTH_SCREENVERTS_COMPONENT_COUNT = VERTEX_BUFFER_MAX,
DEPTH_SCREENVERTS_COMPONENT_BYTES = DEPTH_SCREENVERTS_COMPONENT_COUNT * sizeof(int) + 384,
DEPTH_SCREENVERTS_TOTAL_BYTES = DEPTH_SCREENVERTS_COMPONENT_BYTES * 3,
DEPTH_INDEXBUFFER_BYTES = DEPTH_TRANSFORMED_MAX_VERTS * 3 * sizeof(uint16_t), // hmmm
};
// We process vertices for depth rendering in several stages:
// First, we transform and collect vertices into depthTransformed_ (4-vectors, xyzw).
// Then, we group and cull the vertices into four-triangle groups, which are placed in
// depthScreenVerts_, with x, y and z separated into different part of the array.
// (Alternatively, if drawing rectangles, they're just added linearly).
// After that, we send these groups out for SIMD setup and rasterization.
void DrawEngineCommon::InitDepthRaster() {
switch ((DepthRasterMode)g_Config.iDepthRasterMode) {
case DepthRasterMode::DEFAULT:
case DepthRasterMode::LOW_QUALITY:
useDepthRaster_ = PSP_CoreParameter().compat.flags().SoftwareRasterDepth;
break;
case DepthRasterMode::FORCE_ON:
useDepthRaster_ = true;
break;
case DepthRasterMode::OFF:
useDepthRaster_ = false;
}
if (useDepthRaster_) {
depthDraws_.reserve(256);
depthTransformed_ = (float *)AllocateMemoryPages(DEPTH_TRANSFORMED_BYTES, MEM_PROT_READ | MEM_PROT_WRITE);
depthScreenVerts_ = (int *)AllocateMemoryPages(DEPTH_SCREENVERTS_TOTAL_BYTES, MEM_PROT_READ | MEM_PROT_WRITE);
depthIndices_ = (uint16_t *)AllocateMemoryPages(DEPTH_INDEXBUFFER_BYTES, MEM_PROT_READ | MEM_PROT_WRITE);
}
}
void DrawEngineCommon::ShutdownDepthRaster() {
if (depthTransformed_) {
FreeMemoryPages(depthTransformed_, DEPTH_TRANSFORMED_BYTES);
}
if (depthScreenVerts_) {
FreeMemoryPages(depthScreenVerts_, DEPTH_SCREENVERTS_TOTAL_BYTES);
}
if (depthIndices_) {
FreeMemoryPages(depthIndices_, DEPTH_INDEXBUFFER_BYTES);
}
}
Mat4F32 ComputeFinalProjMatrix() {
const float viewportTranslate[4] = {
gstate.getViewportXCenter() - gstate.getOffsetX(),
gstate.getViewportYCenter() - gstate.getOffsetY(),
gstate.getViewportZCenter(),
0.0f,
};
Mat4F32 wv = Mul4x3By4x4(Mat4x3F32(gstate.worldMatrix), Mat4F32::Load4x3(gstate.viewMatrix));
Mat4F32 m = Mul4x4By4x4(wv, Mat4F32(gstate.projMatrix));
// NOTE: Applying the translation actually works pre-divide, since W is also affected.
Vec4F32 scale = Vec4F32::LoadF24x3_One(&gstate.viewportxscale);
Vec4F32 translate = Vec4F32::Load(viewportTranslate);
TranslateAndScaleInplace(m, scale, translate);
return m;
}
bool DrawEngineCommon::CalculateDepthDraw(DepthDraw *draw, GEPrimitiveType prim, int vertexCount) {
switch (prim) {
case GE_PRIM_INVALID:
case GE_PRIM_KEEP_PREVIOUS:
case GE_PRIM_LINES:
case GE_PRIM_LINE_STRIP:
case GE_PRIM_POINTS:
return false;
default:
break;
}
// Ignore some useless compare modes.
switch (gstate.getDepthTestFunction()) {
case GE_COMP_ALWAYS:
draw->compareMode = ZCompareMode::Always;
break;
case GE_COMP_LEQUAL:
case GE_COMP_LESS:
draw->compareMode = ZCompareMode::Less;
break;
case GE_COMP_GEQUAL:
case GE_COMP_GREATER:
draw->compareMode = ZCompareMode::Greater; // Most common
break;
case GE_COMP_NEVER:
case GE_COMP_EQUAL:
// These will never have a useful effect in Z-only raster.
[[fallthrough]];
case GE_COMP_NOTEQUAL:
// This is highly unusual, let's just ignore it.
[[fallthrough]];
default:
return false;
}
if (gstate.isModeClear()) {
if (!gstate.isClearModeDepthMask()) {
return false;
}
draw->compareMode = ZCompareMode::Always;
} else {
// These should have been caught earlier.
_dbg_assert_(gstate.isDepthTestEnabled());
_dbg_assert_(gstate.isDepthWriteEnabled());
}
if (depthVertexCount_ + vertexCount >= DEPTH_TRANSFORMED_MAX_VERTS) {
// Can't add more. We need to flush.
return false;
}
draw->depthAddr = gstate.getDepthBufRawAddress() | 0x04000000;
draw->depthStride = gstate.DepthBufStride();
draw->vertexOffset = depthVertexCount_;
draw->indexOffset = depthIndexCount_;
draw->vertexCount = vertexCount;
draw->cullEnabled = gstate.isCullEnabled();
draw->cullMode = gstate.getCullMode();
draw->prim = prim;
draw->scissor.x1 = gstate.getScissorX1();
draw->scissor.y1 = gstate.getScissorY1();
draw->scissor.x2 = gstate.getScissorX2();
draw->scissor.y2 = gstate.getScissorY2();
return true;
}
void DrawEngineCommon::DepthRasterSubmitRaw(GEPrimitiveType prim, const VertexDecoder *dec, uint32_t vertTypeID, int vertexCount) {
if (!gstate.isModeClear() && (!gstate.isDepthTestEnabled() || !gstate.isDepthWriteEnabled())) {
return;
}
if (vertTypeID & (GE_VTYPE_WEIGHT_MASK | GE_VTYPE_MORPHCOUNT_MASK)) {
return;
}
_dbg_assert_(prim != GE_PRIM_RECTANGLES);
float worldviewproj[16];
ComputeFinalProjMatrix().Store(worldviewproj);
DepthDraw draw;
if (!CalculateDepthDraw(&draw, prim, vertexCount)) {
return;
}
TimeCollector collectStat(&gpuStats.perFrame.msPrepareDepth, coreCollectDebugStats);
// Decode.
int numDecoded = 0;
for (int i = 0; i < numDrawVerts_; i++) {
const DeferredVerts &dv = drawVerts_[i];
if (dv.indexUpperBound + 1 - dv.indexLowerBound + numDecoded >= DEPTH_TRANSFORMED_MAX_VERTS) {
// Hit our limit! Stop decoding in this draw.
// We should have already broken out in CalculateDepthDraw.
break;
}
// Decode the verts (and at the same time apply morphing/skinning). Simple.
DecodeAndTransformForDepthRaster(depthTransformed_ + (draw.vertexOffset + numDecoded) * 4, worldviewproj, dv.verts, dv.indexLowerBound, dv.indexUpperBound, dec, vertTypeID);
numDecoded += dv.indexUpperBound - dv.indexLowerBound + 1;
}
// Copy indices.
memcpy(depthIndices_ + draw.indexOffset, decIndex_, sizeof(uint16_t) * vertexCount);
// Commit
depthIndexCount_ += vertexCount;
depthVertexCount_ += numDecoded;
if (depthDraws_.empty()) {
rasterTimeStart_ = time_now_d();
}
depthDraws_.push_back(draw);
// FlushQueuedDepth();
}
void DrawEngineCommon::DepthRasterPredecoded(GEPrimitiveType prim, const void *inVerts, int numDecoded, const VertexDecoder *dec, int vertexCount) {
if (!gstate.isModeClear() && (!gstate.isDepthTestEnabled() || !gstate.isDepthWriteEnabled())) {
return;
}
DepthDraw draw;
if (!CalculateDepthDraw(&draw, prim, vertexCount)) {
return;
}
TimeCollector collectStat(&gpuStats.perFrame.msPrepareDepth, coreCollectDebugStats);
// Make sure these have already been indexed away.
_dbg_assert_(prim != GE_PRIM_TRIANGLE_STRIP && prim != GE_PRIM_TRIANGLE_FAN);
if (dec->throughmode) {
ConvertPredecodedThroughForDepthRaster(depthTransformed_ + 4 * draw.vertexOffset, decoded_, dec, numDecoded);
} else {
if (dec->VertexType() & (GE_VTYPE_WEIGHT_MASK | GE_VTYPE_MORPHCOUNT_MASK)) {
return;
}
float worldviewproj[16];
ComputeFinalProjMatrix().Store(worldviewproj);
TransformPredecodedForDepthRaster(depthTransformed_ + 4 * draw.vertexOffset, worldviewproj, decoded_, dec, numDecoded);
}
// Copy indices.
memcpy(depthIndices_ + draw.indexOffset, decIndex_, sizeof(uint16_t) * vertexCount);
// Commit
depthIndexCount_ += vertexCount;
depthVertexCount_ += numDecoded;
depthDraws_.push_back(draw);
if (depthDraws_.empty()) {
rasterTimeStart_ = time_now_d();
}
// FlushQueuedDepth();
}
void DrawEngineCommon::FlushQueuedDepth() {
if (rasterTimeStart_ != 0.0) {
gpuStats.perFrame.msRasterTimeAvailable += time_now_d() - rasterTimeStart_;
rasterTimeStart_ = 0.0;
}
const bool collectStats = coreCollectDebugStats;
const bool lowQ = g_Config.iDepthRasterMode == (int)DepthRasterMode::LOW_QUALITY;
for (const auto &draw : depthDraws_) {
int *tx = depthScreenVerts_;
int *ty = depthScreenVerts_ + DEPTH_SCREENVERTS_COMPONENT_COUNT;
float *tz = (float *)(depthScreenVerts_ + DEPTH_SCREENVERTS_COMPONENT_COUNT * 2);
int outVertCount = 0;
const float *vertices = depthTransformed_ + 4 * draw.vertexOffset;
const uint16_t *indices = depthIndices_ + draw.indexOffset;
DepthScissor tileScissor = draw.scissor.Tile(0, 1);
{
TimeCollector collectStat(&gpuStats.perFrame.msCullDepth, collectStats);
switch (draw.prim) {
case GE_PRIM_RECTANGLES:
outVertCount = DepthRasterClipIndexedRectangles(tx, ty, tz, vertices, indices, draw, tileScissor);
break;
case GE_PRIM_TRIANGLES:
outVertCount = DepthRasterClipIndexedTriangles(tx, ty, tz, vertices, indices, draw, tileScissor);
break;
default:
_dbg_assert_(false);
break;
}
}
if (outVertCount > 0) {
TimeCollector collectStat(&gpuStats.perFrame.msRasterizeDepth, collectStats);
if (!Memory::IsValid4AlignedAddress(draw.depthAddr)) {
continue;
}
u16 *depthPtr = (uint16_t *)Memory::GetPointerWriteUnchecked(draw.depthAddr);
DepthRasterScreenVerts(depthPtr, draw.depthStride, tx, ty, tz, outVertCount, draw, tileScissor, lowQ);
}
}
// Reset queue
depthIndexCount_ = 0;
depthVertexCount_ = 0;
depthDraws_.clear();
}