Files
ppsspp/GPU/Software/SoftGpu.cpp
Henrik RydgårdandClaude Opus 5 d2762328f0 softgpu: Write down what's left from the consistency audit
TODO list at the top of SoftGpu.cpp covering the remaining UB and the platform
differences we haven't settled yet, with enough detail to act on each without
re-deriving it. Also drops a duplicated GPUCommon.h include in SoftGpu.h.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
2026-08-29 12:30:46 +02:00

1498 lines
59 KiB
C++

// Copyright (c) 2012- 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 <set>
#include "Common/System/Display.h"
#include "Common/GPU/OpenGL/GLFeatures.h"
#include "GPU/GPUState.h"
#include "GPU/ge_constants.h"
#include "GPU/Common/TextureDecoder.h"
#include "Common/Data/Convert/ColorConv.h"
#include "Common/GPU/GraphicsContext.h"
#include "Common/LogReporting.h"
#include "Core/Config.h"
#include "Core/ConfigValues.h"
#include "Core/Core.h"
#include "Core/System.h"
#include "Core/Debugger/MemBlockInfo.h"
#include "Core/MemMap.h"
#include "Core/MIPS/MIPS.h"
#include "Core/Util/PPGeDraw.h"
#include "Common/Profiler/Profiler.h"
#include "Common/GPU/thin3d.h"
#include "GPU/GPUCommon.h"
#include "GPU/Software/DrawPixel.h"
#include "GPU/Software/Rasterizer.h"
#include "GPU/Software/Sampler.h"
#include "GPU/Software/SoftGpu.h"
#include "GPU/Software/TransformUnit.h"
#include "GPU/Common/DrawEngineCommon.h"
#include "GPU/Common/PresentationCommon.h"
#include "Common/GPU/ShaderTranslation.h"
#include "GPU/Common/SplineCommon.h"
#include "GPU/Debugger/Record.h"
// TODO list for the software renderer, from the platform-consistency audit.
//
// The goal these all serve: the softgpu should produce bit-identical output on every platform, so
// that comparing against the pspautotests rendering tests measures emulation accuracy rather than
// which compiler and CPU happened to build it. Most of the fixed issues weren't rounding drift -
// they were fallback paths computing something different from the SIMD ones.
//
// Correctness / undefined behavior:
// * BinManager's Add* functions OR into states_[stateIndex_].flags after pushing the item, which
// items dispatched by an earlier Drain() may be reading on a worker thread. Optimizing the states
// is now confined to the drained window, but this one needs the state entries to become
// copy-on-write once anything referencing them has been dispatched.
// * ClipToScreenInternal converts out-of-range and NaN floats to int. x86 gives 0x80000000 while
// AArch64 saturates to 0x7FFFFFFF - which is exactly ClipVertexData::OutsideRange()'s sentinel,
// so ARM64 discards triangles that x86 draws. Reachable for any vertex behind the near plane
// with depth clip enabled.
// * The edge functions in Rasterizer.cpp and the back-face cross product in BinManager.cpp can
// overflow int - screen coords are 1/16 fixed point and through-mode coords reach +-524288, so
// the products pass INT_MAX. The sign of an overflowed expression decides whether we cull.
// * Collinear triangles aren't culled (AddTriangle only rejects cross < 0), so wsum_recip is inf
// and the interpolators feed inf/NaN into Cast<int>().
// * The CLUT index plus the per-mip level offset can reach 623 in a 512-entry table. The JIT
// reproduces the overrun exactly, so the sampled color is whatever links after clut[].
// * 32-bit vertex indices above 0xFFFF index out of bounds: GetIndexBounds truncates the bounds to
// uint16_t and only logs, while IndexConverter returns the full u32.
//
// Remaining platform differences:
// * Vec4<float>::ToRGBA and friends (Math3D.h) convert float->u8 with _mm_cvtps_epi32 under SSE and
// vcvtq_s32_f32 under NEON - rounding versus truncating, same split we fixed for depth. Rounding
// is probably the correct answer for a 0..1 -> 0..255 conversion, so this needs a decision rather
// than a mechanical fix, and it changes x86 output either way.
// * pspLightPow still calls std::pow, which isn't correctly rounded and so differs between libm
// implementations; see the note there.
// * -ffp-contract=off is set for these files in GPU/CMakeLists.txt, which covers the CMake builds
// including Android's. The legacy android/jni ndk-build and libretro/Makefile.common have no
// per-file mechanism and are still contracting.
constexpr int FB_WIDTH = 480;
constexpr int FB_HEIGHT = 272;
uint8_t clut[1024];
FormatBuffer fb;
FormatBuffer depthbuf;
struct CommandInfo {
uint64_t flags;
SoftGPU::CmdFunc func;
};
static CommandInfo softgpuCmdInfo[256];
struct SoftwareCommandTableEntry {
uint8_t cmd;
uint8_t flags;
SoftDirty dirty;
SoftGPU::CmdFunc func;
};
// Software uses a different one, because dirty flags and execute funcs are a bit different.
const SoftwareCommandTableEntry softgpuCommandTable[] = {
{ GE_CMD_OFFSETADDR, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_OffsetAddr },
{ GE_CMD_ORIGIN, FLAG_EXECUTE | FLAG_READS_PC, SoftDirty::NONE, &GPUCommon::Execute_Origin },
{ GE_CMD_JUMP, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, SoftDirty::NONE, &GPUCommon::Execute_Jump },
{ GE_CMD_CALL, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, SoftDirty::NONE, &SoftGPU::Execute_Call },
{ GE_CMD_RET, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, SoftDirty::NONE, &GPUCommon::Execute_Ret },
{ GE_CMD_END, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, SoftDirty::NONE, &GPUCommon::Execute_End },
{ GE_CMD_VADDR, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Vaddr },
{ GE_CMD_IADDR, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Iaddr },
{ GE_CMD_BJUMP, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, SoftDirty::NONE, &GPUCommon::Execute_BJump },
{ GE_CMD_BOUNDINGBOX, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_BoundingBox },
{ GE_CMD_PRIM, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_Prim },
{ GE_CMD_BEZIER, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_Bezier },
{ GE_CMD_SPLINE, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_Spline },
// Vertex type affects a number of things, mainly because of through.
{ GE_CMD_VERTEXTYPE, FLAG_EXECUTEONCHANGE, SoftDirty::TRANSFORM_BASIC, &SoftGPU::Execute_VertexType },
{ GE_CMD_LOADCLUT, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_LoadClut },
// These two are actually processed in CMD_END, no flush needed.
{ GE_CMD_SIGNAL },
{ GE_CMD_FINISH },
// Changes that dirty the framebuffer or depthbuffer pointer/size.
{ GE_CMD_FRAMEBUFPTR, FLAG_EXECUTEONCHANGE, SoftDirty::BINNER_RANGE, &SoftGPU::Execute_FramebufPtr },
{ GE_CMD_FRAMEBUFWIDTH, FLAG_EXECUTEONCHANGE, SoftDirty::BINNER_RANGE | SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_CACHED, &SoftGPU::Execute_FramebufPtr },
{ GE_CMD_FRAMEBUFPIXFORMAT, FLAG_EXECUTEONCHANGE, SoftDirty::BINNER_RANGE | SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_STENCIL | SoftDirty::PIXEL_WRITEMASK, &SoftGPU::Execute_FramebufFormat },
{ GE_CMD_ZBUFPTR, FLAG_EXECUTEONCHANGE, SoftDirty::BINNER_RANGE, &SoftGPU::Execute_ZbufPtr },
{ GE_CMD_ZBUFWIDTH, FLAG_EXECUTEONCHANGE, SoftDirty::BINNER_RANGE | SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_CACHED, &SoftGPU::Execute_ZbufPtr },
{ GE_CMD_FOGCOLOR, 0, SoftDirty::PIXEL_CACHED },
{ GE_CMD_FOG1, 0, SoftDirty::TRANSFORM_FOG },
{ GE_CMD_FOG2, 0, SoftDirty::TRANSFORM_FOG },
{ GE_CMD_CLEARMODE, 0, SoftDirty::TRANSFORM_BASIC | SoftDirty::RAST_BASIC | SoftDirty::RAST_TEX | SoftDirty::SAMPLER_BASIC | SoftDirty::SAMPLER_TEXLIST | SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_ALPHA | SoftDirty::PIXEL_STENCIL | SoftDirty::PIXEL_CACHED | SoftDirty::BINNER_RANGE | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXTUREMAPENABLE, 0, SoftDirty::SAMPLER_BASIC | SoftDirty::SAMPLER_TEXLIST | SoftDirty::RAST_TEX | SoftDirty::TRANSFORM_BASIC | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_FOGENABLE, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_CACHED | SoftDirty::TRANSFORM_BASIC | SoftDirty::TRANSFORM_FOG | SoftDirty::TRANSFORM_MATRIX },
{ GE_CMD_TEXMODE, 0, SoftDirty::SAMPLER_BASIC | SoftDirty::SAMPLER_TEXLIST | SoftDirty::RAST_TEX },
// Currently this doesn't affect any state, but maybe it should.
{ GE_CMD_TEXSHADELS },
{ GE_CMD_SHADEMODE, 0, SoftDirty::RAST_BASIC },
{ GE_CMD_TEXFUNC, 0, SoftDirty::SAMPLER_BASIC },
{ GE_CMD_COLORTEST, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_CACHED },
{ GE_CMD_ALPHATESTENABLE, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_ALPHA },
{ GE_CMD_COLORTESTENABLE, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_CACHED },
{ GE_CMD_COLORTESTMASK, 0, SoftDirty::PIXEL_CACHED },
{ GE_CMD_REVERSENORMAL, 0, SoftDirty::TRANSFORM_BASIC },
{ GE_CMD_LIGHTINGENABLE, 0, SoftDirty::TRANSFORM_BASIC | SoftDirty::TRANSFORM_MATRIX | SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_0 | SoftDirty::LIGHT_1 | SoftDirty::LIGHT_2 | SoftDirty::LIGHT_3 },
{ GE_CMD_LIGHTENABLE0, 0, SoftDirty::TRANSFORM_MATRIX | SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_0 },
{ GE_CMD_LIGHTENABLE1, 0, SoftDirty::TRANSFORM_MATRIX | SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_1 },
{ GE_CMD_LIGHTENABLE2, 0, SoftDirty::TRANSFORM_MATRIX | SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_2 },
{ GE_CMD_LIGHTENABLE3, 0, SoftDirty::TRANSFORM_MATRIX | SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_3 },
{ GE_CMD_LIGHTTYPE0, 0, SoftDirty::TRANSFORM_MATRIX | SoftDirty::LIGHT_0 },
{ GE_CMD_LIGHTTYPE1, 0, SoftDirty::TRANSFORM_MATRIX | SoftDirty::LIGHT_1 },
{ GE_CMD_LIGHTTYPE2, 0, SoftDirty::TRANSFORM_MATRIX | SoftDirty::LIGHT_2 },
{ GE_CMD_LIGHTTYPE3, 0, SoftDirty::TRANSFORM_MATRIX | SoftDirty::LIGHT_3 },
{ GE_CMD_MATERIALUPDATE, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL },
{ GE_CMD_LIGHTMODE, 0, SoftDirty::LIGHT_BASIC },
{ GE_CMD_TEXFILTER, 0, SoftDirty::SAMPLER_BASIC | SoftDirty::SAMPLER_TEXLIST | SoftDirty::RAST_TEX },
{ GE_CMD_TEXWRAP, 0, SoftDirty::SAMPLER_BASIC },
{ GE_CMD_ALPHATEST, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_ALPHA },
{ GE_CMD_COLORREF, 0, SoftDirty::PIXEL_CACHED },
{ GE_CMD_TEXENVCOLOR, 0, SoftDirty::PIXEL_CACHED },
// Currently, this is not part of state, just read on vertex processing.
{ GE_CMD_CULL },
{ GE_CMD_CULLFACEENABLE },
{ GE_CMD_DITHERENABLE, 0, SoftDirty::PIXEL_BASIC },
{ GE_CMD_STENCILOP, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_STENCIL },
{ GE_CMD_STENCILTEST, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_STENCIL },
{ GE_CMD_STENCILTESTENABLE, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_STENCIL },
{ GE_CMD_ALPHABLENDENABLE, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_ALPHA },
{ GE_CMD_BLENDMODE, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_ALPHA },
{ GE_CMD_BLENDFIXEDA, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_ALPHA | SoftDirty::PIXEL_CACHED },
{ GE_CMD_BLENDFIXEDB, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_ALPHA | SoftDirty::PIXEL_CACHED },
{ GE_CMD_MASKRGB, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_WRITEMASK },
{ GE_CMD_MASKALPHA, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_WRITEMASK },
{ GE_CMD_ZTEST, 0, SoftDirty::PIXEL_BASIC },
{ GE_CMD_ZTESTENABLE, 0, SoftDirty::PIXEL_BASIC | SoftDirty::BINNER_RANGE | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_ZWRITEDISABLE, 0, SoftDirty::PIXEL_BASIC | SoftDirty::BINNER_RANGE | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_LOGICOP, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_CACHED },
{ GE_CMD_LOGICOPENABLE, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_CACHED },
{ GE_CMD_TEXMAPMODE, 0, SoftDirty::TRANSFORM_BASIC | SoftDirty::RAST_TEX },
// These are read on every SubmitPrim, no need for dirtying or flushing.
{ GE_CMD_TEXSCALEU },
{ GE_CMD_TEXSCALEV },
{ GE_CMD_TEXOFFSETU },
{ GE_CMD_TEXOFFSETV },
{ GE_CMD_TEXSIZE0, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXSIZE1, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXSIZE2, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXSIZE3, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXSIZE4, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXSIZE5, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXSIZE6, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXSIZE7, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXFORMAT, 0, SoftDirty::SAMPLER_BASIC | SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXLEVEL, 0, SoftDirty::RAST_TEX },
{ GE_CMD_TEXLODSLOPE, 0, SoftDirty::RAST_TEX },
{ GE_CMD_TEXADDR0, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXADDR1, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXADDR2, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXADDR3, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXADDR4, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXADDR5, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXADDR6, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXADDR7, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXBUFWIDTH0, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXBUFWIDTH1, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXBUFWIDTH2, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXBUFWIDTH3, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXBUFWIDTH4, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXBUFWIDTH5, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXBUFWIDTH6, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_TEXBUFWIDTH7, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
{ GE_CMD_CLUTADDR },
{ GE_CMD_CLUTADDRUPPER },
{ GE_CMD_CLUTFORMAT, 0, SoftDirty::SAMPLER_BASIC },
// Morph weights. TODO: Remove precomputation?
{ GE_CMD_MORPHWEIGHT0, FLAG_EXECUTEONCHANGE, SoftDirty::NONE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT1, FLAG_EXECUTEONCHANGE, SoftDirty::NONE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT2, FLAG_EXECUTEONCHANGE, SoftDirty::NONE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT3, FLAG_EXECUTEONCHANGE, SoftDirty::NONE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT4, FLAG_EXECUTEONCHANGE, SoftDirty::NONE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT5, FLAG_EXECUTEONCHANGE, SoftDirty::NONE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT6, FLAG_EXECUTEONCHANGE, SoftDirty::NONE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT7, FLAG_EXECUTEONCHANGE, SoftDirty::NONE, &GPUCommon::Execute_MorphWeight },
// No state of flushing required for patch parameters, currently.
{ GE_CMD_PATCHDIVISION },
{ GE_CMD_PATCHPRIMITIVE },
{ GE_CMD_PATCHFACING },
{ GE_CMD_PATCHCULLENABLE },
// Can probably ignore this one as we don't support AA lines.
{ GE_CMD_ANTIALIASENABLE, 0, SoftDirty::RAST_BASIC },
// Viewport and offset for positions.
{ GE_CMD_OFFSETX, 0, SoftDirty::RAST_OFFSET },
{ GE_CMD_OFFSETY, 0, SoftDirty::RAST_OFFSET },
{ GE_CMD_VIEWPORTXSCALE, 0, SoftDirty::TRANSFORM_VIEWPORT },
{ GE_CMD_VIEWPORTYSCALE, 0, SoftDirty::TRANSFORM_VIEWPORT },
{ GE_CMD_VIEWPORTXCENTER, 0, SoftDirty::TRANSFORM_VIEWPORT },
{ GE_CMD_VIEWPORTYCENTER, 0, SoftDirty::TRANSFORM_VIEWPORT },
{ GE_CMD_VIEWPORTZSCALE, 0, SoftDirty::TRANSFORM_VIEWPORT },
{ GE_CMD_VIEWPORTZCENTER, 0, SoftDirty::TRANSFORM_VIEWPORT },
{ GE_CMD_DEPTHCLIPENABLE, 0, SoftDirty::TRANSFORM_BASIC },
// Z clipping.
{ GE_CMD_MINZ, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_CACHED },
{ GE_CMD_MAXZ, 0, SoftDirty::PIXEL_BASIC | SoftDirty::PIXEL_CACHED },
// Region doesn't seem to affect scissor or anything.
// As long as REGION1 is zero, REGION2 is effectively another scissor.
{ GE_CMD_REGION1, 0, SoftDirty::BINNER_RANGE },
{ GE_CMD_REGION2, 0, SoftDirty::BINNER_RANGE },
// Scissor, only used by the binner.
{ GE_CMD_SCISSOR1, 0, SoftDirty::BINNER_RANGE },
{ GE_CMD_SCISSOR2, 0, SoftDirty::BINNER_RANGE },
// Lighting base colors.
{ GE_CMD_AMBIENTCOLOR, 0, SoftDirty::LIGHT_MATERIAL },
{ GE_CMD_AMBIENTALPHA, 0, SoftDirty::LIGHT_MATERIAL },
{ GE_CMD_MATERIALDIFFUSE, 0, SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_0 | SoftDirty::LIGHT_1 | SoftDirty::LIGHT_2 | SoftDirty::LIGHT_3 },
// Not currently state, but maybe should be.
{ GE_CMD_MATERIALEMISSIVE, 0, SoftDirty::NONE },
{ GE_CMD_MATERIALAMBIENT, 0, SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_0 | SoftDirty::LIGHT_1 | SoftDirty::LIGHT_2 | SoftDirty::LIGHT_3 },
{ GE_CMD_MATERIALALPHA, 0, SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_0 | SoftDirty::LIGHT_1 | SoftDirty::LIGHT_2 | SoftDirty::LIGHT_3 },
{ GE_CMD_MATERIALSPECULAR, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_0 | SoftDirty::LIGHT_1 | SoftDirty::LIGHT_2 | SoftDirty::LIGHT_3 },
{ GE_CMD_MATERIALSPECULARCOEF, 0, SoftDirty::LIGHT_BASIC },
{ GE_CMD_LX0, 0, SoftDirty::LIGHT_0 },
{ GE_CMD_LY0, 0, SoftDirty::LIGHT_0 },
{ GE_CMD_LZ0, 0, SoftDirty::LIGHT_0 },
{ GE_CMD_LX1, 0, SoftDirty::LIGHT_1 },
{ GE_CMD_LY1, 0, SoftDirty::LIGHT_1 },
{ GE_CMD_LZ1, 0, SoftDirty::LIGHT_1 },
{ GE_CMD_LX2, 0, SoftDirty::LIGHT_2 },
{ GE_CMD_LY2, 0, SoftDirty::LIGHT_2 },
{ GE_CMD_LZ2, 0, SoftDirty::LIGHT_2 },
{ GE_CMD_LX3, 0, SoftDirty::LIGHT_3 },
{ GE_CMD_LY3, 0, SoftDirty::LIGHT_3 },
{ GE_CMD_LZ3, 0, SoftDirty::LIGHT_3 },
{ GE_CMD_LDX0, 0, SoftDirty::LIGHT_0 },
{ GE_CMD_LDY0, 0, SoftDirty::LIGHT_0 },
{ GE_CMD_LDZ0, 0, SoftDirty::LIGHT_0 },
{ GE_CMD_LDX1, 0, SoftDirty::LIGHT_1 },
{ GE_CMD_LDY1, 0, SoftDirty::LIGHT_1 },
{ GE_CMD_LDZ1, 0, SoftDirty::LIGHT_1 },
{ GE_CMD_LDX2, 0, SoftDirty::LIGHT_2 },
{ GE_CMD_LDY2, 0, SoftDirty::LIGHT_2 },
{ GE_CMD_LDZ2, 0, SoftDirty::LIGHT_2 },
{ GE_CMD_LDX3, 0, SoftDirty::LIGHT_3 },
{ GE_CMD_LDY3, 0, SoftDirty::LIGHT_3 },
{ GE_CMD_LDZ3, 0, SoftDirty::LIGHT_3 },
{ GE_CMD_LKA0, 0, SoftDirty::LIGHT_0 },
{ GE_CMD_LKB0, 0, SoftDirty::LIGHT_0 },
{ GE_CMD_LKC0, 0, SoftDirty::LIGHT_0 },
{ GE_CMD_LKA1, 0, SoftDirty::LIGHT_1 },
{ GE_CMD_LKB1, 0, SoftDirty::LIGHT_1 },
{ GE_CMD_LKC1, 0, SoftDirty::LIGHT_1 },
{ GE_CMD_LKA2, 0, SoftDirty::LIGHT_2 },
{ GE_CMD_LKB2, 0, SoftDirty::LIGHT_2 },
{ GE_CMD_LKC2, 0, SoftDirty::LIGHT_2 },
{ GE_CMD_LKA3, 0, SoftDirty::LIGHT_3 },
{ GE_CMD_LKB3, 0, SoftDirty::LIGHT_3 },
{ GE_CMD_LKC3, 0, SoftDirty::LIGHT_3 },
{ GE_CMD_LKS0, 0, SoftDirty::LIGHT_0 },
{ GE_CMD_LKS1, 0, SoftDirty::LIGHT_1 },
{ GE_CMD_LKS2, 0, SoftDirty::LIGHT_2 },
{ GE_CMD_LKS3, 0, SoftDirty::LIGHT_3 },
{ GE_CMD_LKO0, 0, SoftDirty::LIGHT_0 },
{ GE_CMD_LKO1, 0, SoftDirty::LIGHT_1 },
{ GE_CMD_LKO2, 0, SoftDirty::LIGHT_2 },
{ GE_CMD_LKO3, 0, SoftDirty::LIGHT_3 },
// Specific light colors.
{ GE_CMD_LAC0, 0, SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_0 },
{ GE_CMD_LDC0, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_0 },
{ GE_CMD_LSC0, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_0 },
{ GE_CMD_LAC1, 0, SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_1 },
{ GE_CMD_LDC1, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_1 },
{ GE_CMD_LSC1, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_1 },
{ GE_CMD_LAC2, 0, SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_2 },
{ GE_CMD_LDC2, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_2 },
{ GE_CMD_LSC2, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_2 },
{ GE_CMD_LAC3, 0, SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_3 },
{ GE_CMD_LDC3, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_3 },
{ GE_CMD_LSC3, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_3 },
// These are currently ignored, but might do flushing later.
{ GE_CMD_TEXFLUSH },
{ GE_CMD_TEXSYNC },
// These are just nop or part of other later commands.
{ GE_CMD_NOP },
{ GE_CMD_BASE },
{ GE_CMD_TRANSFERSRC },
{ GE_CMD_TRANSFERSRCW },
{ GE_CMD_TRANSFERDST },
{ GE_CMD_TRANSFERDSTW },
{ GE_CMD_TRANSFERSRCPOS },
{ GE_CMD_TRANSFERDSTPOS },
{ GE_CMD_TRANSFERSIZE },
// This will flush if necessary.
{ GE_CMD_TRANSFERSTART, FLAG_EXECUTE | FLAG_READS_PC, SoftDirty::NONE, &SoftGPU::Execute_BlockTransferStart },
// We cache the dither matrix, but the values affect little.
{ GE_CMD_DITH0, 0, SoftDirty::PIXEL_DITHER },
{ GE_CMD_DITH1, 0, SoftDirty::PIXEL_DITHER },
{ GE_CMD_DITH2, 0, SoftDirty::PIXEL_DITHER },
{ GE_CMD_DITH3, 0, SoftDirty::PIXEL_DITHER },
{ GE_CMD_WORLDMATRIXNUMBER, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_WorldMtxNum },
{ GE_CMD_WORLDMATRIXDATA, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_WorldMtxData },
{ GE_CMD_VIEWMATRIXNUMBER, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_ViewMtxNum },
{ GE_CMD_VIEWMATRIXDATA, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_ViewMtxData },
{ GE_CMD_PROJMATRIXNUMBER, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_ProjMtxNum },
{ GE_CMD_PROJMATRIXDATA, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_ProjMtxData },
// Currently not state.
{ GE_CMD_TGENMATRIXNUMBER, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_TgenMtxNum },
{ GE_CMD_TGENMATRIXDATA, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_TgenMtxData },
{ GE_CMD_BONEMATRIXNUMBER, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_BoneMtxNum },
{ GE_CMD_BONEMATRIXDATA, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_BoneMtxData },
// Vertex Screen/Texture/Color
{ GE_CMD_VSCX },
{ GE_CMD_VSCY },
{ GE_CMD_VSCZ },
{ GE_CMD_VTCS },
{ GE_CMD_VTCT },
{ GE_CMD_VTCQ },
{ GE_CMD_VCV },
{ GE_CMD_VAP, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_ImmVertexAlphaPrim },
{ GE_CMD_VFC },
{ GE_CMD_VSCV },
// "Missing" commands (gaps in the sequence)
{ GE_CMD_UNKNOWN_03, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_0D, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_11, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_29, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_34, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_35, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_39, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_4E, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_4F, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_52, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_59, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_5A, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_B6, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_B7, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_D1, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_ED, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_EF, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_FA, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_FB, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_FC, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_FD, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
{ GE_CMD_UNKNOWN_FE, FLAG_EXECUTE, SoftDirty::NONE, &GPUCommon::Execute_Unknown },
// Appears to be debugging related or something? Hit a lot in GoW.
{ GE_CMD_NOP_FF },
};
SoftGPU::SoftGPU(GraphicsContext *gfxCtx, Draw::DrawContext *draw)
: GPUCommon(gfxCtx, draw)
{
fb.data = Memory::GetPointerWriteOrException(0x44000000); // TODO: correct default address?
depthbuf.data = Memory::GetPointerWriteOrException(0x44000000); // TODO: correct default address?
memset(softgpuCmdInfo, 0, sizeof(softgpuCmdInfo));
// Convert the command table to a faster format, and check for dupes.
std::set<u8> dupeCheck;
for (size_t i = 0; i < ARRAY_SIZE(softgpuCommandTable); i++) {
const u8 cmd = softgpuCommandTable[i].cmd;
if (dupeCheck.find(cmd) != dupeCheck.end()) {
ERROR_LOG(Log::G3D, "Command table Dupe: %02x (%i)", (int)cmd, (int)cmd);
} else {
dupeCheck.insert(cmd);
}
softgpuCmdInfo[cmd].flags |= (uint64_t)softgpuCommandTable[i].flags | ((uint64_t)softgpuCommandTable[i].dirty << 8);
softgpuCmdInfo[cmd].func = softgpuCommandTable[i].func;
if ((softgpuCmdInfo[cmd].flags & (FLAG_EXECUTE | FLAG_EXECUTEONCHANGE)) && !softgpuCmdInfo[cmd].func) {
// Can't have FLAG_EXECUTE commands without a function pointer to execute.
Crash();
}
}
// Find commands missing from the table.
for (int i = 0; i < 0xEF; i++) {
if (dupeCheck.find((u8)i) == dupeCheck.end()) {
ERROR_LOG(Log::G3D, "Command missing from table: %02x (%i)", i, i);
}
}
memset(vramDirty_, (uint8_t)(SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY), sizeof(vramDirty_));
// TODO: Is there a default?
displayFramebuf_ = 0;
displayStride_ = 512;
displayFormat_ = GE_FORMAT_8888;
Rasterizer::Init();
Sampler::Init();
drawEngine_ = new SoftwareDrawEngine();
drawEngine_->SetGPUCommon(this);
drawEngine_->Init();
drawEngineCommon_ = drawEngine_;
// Push the initial CLUT buffer in case it's all zero (we push only on change.)
drawEngine_->transformUnit.NotifyClutUpdate(clut);
// No need to flush for simple parameter changes.
flushOnParams_ = false;
if (gfxCtx && draw) {
presentation_ = new PresentationCommon(draw_);
presentation_->SetLanguage(draw_->GetShaderLanguageDesc().shaderLanguage);
presentation_->UpdateDisplaySize(PSP_CoreParameter().pixelWidth, PSP_CoreParameter().pixelHeight);
presentation_->UpdateRenderSize(PSP_CoreParameter().renderWidth, PSP_CoreParameter().renderHeight);
}
NotifyConfigChanged();
NotifyDisplayResized();
// NotifyRenderResized(); // hopefully not needed here.
}
void SoftGPU::DeviceLost() {
if (presentation_)
presentation_->DeviceLost();
draw_ = nullptr;
if (fbTex) {
fbTex->Release();
fbTex = nullptr;
}
}
void SoftGPU::DeviceRestore(Draw::DrawContext *draw) {
draw_ = draw;
if (presentation_)
presentation_->DeviceRestore(draw_);
PPGeSetDrawContext(draw_);
}
SoftGPU::~SoftGPU() {
if (fbTex) {
fbTex->Release();
fbTex = nullptr;
}
delete presentation_;
delete drawEngine_;
Sampler::Shutdown();
Rasterizer::Shutdown();
}
void SoftGPU::SetDisplayFramebuffer(u32 framebuf, u32 stride, GEBufferFormat format) {
// Seems like this can point into RAM, but should be VRAM if not in RAM.
displayFramebuf_ = (framebuf & 0xFF000000) == 0 ? 0x44000000 | framebuf : framebuf;
displayStride_ = stride;
displayFormat_ = format;
NotifyDisplay(framebuf, stride, format);
}
DSStretch g_DarkStalkerStretch;
void SoftGPU::ConvertTextureDescFrom16(Draw::TextureDesc &desc, int srcwidth, int srcheight, const uint16_t *overrideData) {
// TODO: This should probably be converted in a shader instead..
fbTexBuffer_.resize(srcwidth * srcheight);
const uint16_t *displayBuffer = overrideData;
if (!displayBuffer)
displayBuffer = (const uint16_t *)Memory::GetPointerOrException(displayFramebuf_);
for (int y = 0; y < srcheight; ++y) {
u32 *buf_line = &fbTexBuffer_[y * srcwidth];
const u16 *fb_line = &displayBuffer[y * displayStride_];
switch (displayFormat_) {
case GE_FORMAT_565:
ConvertRGB565ToRGBA8888(buf_line, fb_line, srcwidth);
break;
case GE_FORMAT_5551:
ConvertRGBA5551ToRGBA8888(buf_line, fb_line, srcwidth);
break;
case GE_FORMAT_4444:
ConvertRGBA4444ToRGBA8888(buf_line, fb_line, srcwidth);
break;
default:
ERROR_LOG_REPORT(Log::G3D, "Software: Unexpected framebuffer format: %d", displayFormat_);
break;
}
}
desc.width = srcwidth;
desc.height = srcheight;
desc.initData.push_back((uint8_t *)fbTexBuffer_.data());
}
// Copies RGBA8 data from RAM to the currently bound render target.
void SoftGPU::CopyToCurrentFboFromDisplayRam(const DisplayLayoutConfig &config, int srcwidth, int srcheight) {
if (!draw_ || !presentation_)
return;
float u0 = 0.0f;
float u1;
float v0 = 0.0f;
float v1 = 1.0f;
if (fbTex) {
fbTex->Release();
fbTex = nullptr;
}
// For accuracy, try to handle 0 stride - sometimes used.
if (displayStride_ == 0) {
srcheight = 1;
u1 = 1.0f;
} else {
u1 = (float)srcwidth / displayStride_;
}
Draw::TextureDesc desc{};
desc.type = Draw::TextureType::LINEAR2D;
desc.format = Draw::DataFormat::R8G8B8A8_UNORM;
desc.depth = 1;
desc.mipLevels = 1;
desc.tag = "SoftGPU";
bool hasImage = true;
OutputFlags outputFlags = config.iDisplayFilter == SCALE_NEAREST ? OutputFlags::NEAREST : OutputFlags::LINEAR;
bool hasPostShader = presentation_ && presentation_->HasPostShader();
if (PSP_CoreParameter().compat.flags().DarkStalkersPresentHack && displayFormat_ == GE_FORMAT_5551 && g_DarkStalkerStretch != DSStretch::Off) {
const u8 *data = Memory::GetPointerWriteOrException(0x04088000);
bool fillDesc = true;
if (draw_->GetDataFormatSupport(Draw::DataFormat::A1B5G5R5_UNORM_PACK16) & Draw::FMT_TEXTURE) {
// The perfect one.
desc.format = Draw::DataFormat::A1B5G5R5_UNORM_PACK16;
} else if (!hasPostShader && (draw_->GetDataFormatSupport(Draw::DataFormat::A1R5G5B5_UNORM_PACK16) & Draw::FMT_TEXTURE)) {
// RB swapped, compensate with a shader.
desc.format = Draw::DataFormat::A1R5G5B5_UNORM_PACK16;
outputFlags |= OutputFlags::RB_SWIZZLE;
} else {
ConvertTextureDescFrom16(desc, srcwidth, srcheight, (const uint16_t *)data);
fillDesc = false;
}
if (fillDesc) {
desc.width = displayStride_ == 0 ? srcwidth : displayStride_;
desc.height = srcheight;
desc.initData.push_back(data);
}
u0 = 64.5f / (float)desc.width;
u1 = 447.5f / (float)desc.width;
v0 = 16.0f / (float)desc.height;
v1 = 240.0f / (float)desc.height;
if (g_DarkStalkerStretch == DSStretch::Normal) {
outputFlags |= OutputFlags::PILLARBOX;
}
} else if (!Memory::IsValidAddress(displayFramebuf_) || srcwidth == 0 || srcheight == 0) {
hasImage = false;
u1 = 1.0f;
} else if (displayFormat_ == GE_FORMAT_8888) {
const u8 *data = Memory::GetPointerOrException(displayFramebuf_);
desc.width = displayStride_ == 0 ? srcwidth : displayStride_;
desc.height = srcheight;
desc.initData.push_back(data);
desc.format = Draw::DataFormat::R8G8B8A8_UNORM;
} else if (displayFormat_ == GE_FORMAT_5551) {
const u8 *data = Memory::GetPointerOrException(displayFramebuf_);
bool fillDesc = true;
if (draw_->GetDataFormatSupport(Draw::DataFormat::A1B5G5R5_UNORM_PACK16) & Draw::FMT_TEXTURE) {
// The perfect one.
desc.format = Draw::DataFormat::A1B5G5R5_UNORM_PACK16;
} else if (!hasPostShader && (draw_->GetDataFormatSupport(Draw::DataFormat::A1R5G5B5_UNORM_PACK16) & Draw::FMT_TEXTURE)) {
// RB swapped, compensate with a shader.
desc.format = Draw::DataFormat::A1R5G5B5_UNORM_PACK16;
outputFlags |= OutputFlags::RB_SWIZZLE;
} else {
ConvertTextureDescFrom16(desc, srcwidth, srcheight);
u1 = 1.0f;
fillDesc = false;
}
if (fillDesc) {
desc.width = displayStride_ == 0 ? srcwidth : displayStride_;
desc.height = srcheight;
desc.initData.push_back(data);
}
} else {
ConvertTextureDescFrom16(desc, srcwidth, srcheight);
u1 = 1.0f;
}
if (!hasImage) {
presentation_->SourceBlank();
presentation_->NotifyPresent();
return;
}
fbTex = draw_->CreateTexture(desc);
if (GetGPUBackend() == GPUBackend::OPENGL) {
outputFlags |= OutputFlags::BACKBUFFER_FLIPPED;
}
presentation_->SourceTexture(fbTex, desc.width, desc.height);
presentation_->RunPostshaderPasses(config, outputFlags, config.iInternalScreenRotation, u0, v0, u1, v1);
}
void SoftGPU::PrepareCopyDisplayToOutput(const DisplayLayoutConfig &config) {
drawEngine_->transformUnit.Flush(this, "output");
// The display always shows 480x272.
CopyToCurrentFboFromDisplayRam(config, FB_WIDTH, FB_HEIGHT);
}
void SoftGPU::CopyDisplayToOutput(const DisplayLayoutConfig &config) {
presentation_->CopyToOutput(config);
MarkDirty(displayFramebuf_, displayStride_, 272, displayFormat_, SoftGPUVRAMDirty::CLEAR);
}
void SoftGPU::BeginHostFrame(const DisplayLayoutConfig &config) {
GPUCommon::BeginHostFrame(config);
if (presentation_) {
presentation_->BeginFrame(config);
}
}
bool SoftGPU::PresentedThisFrame() const {
return presentation_ ? presentation_->PresentedThisFrame() : false;
}
void SoftGPU::MarkDirty(uint32_t addr, uint32_t stride, uint32_t height, GEBufferFormat fmt, SoftGPUVRAMDirty value) {
uint32_t bytes = height * stride * (fmt == GE_FORMAT_8888 ? 4 : 2);
MarkDirty(addr, bytes, value);
}
void SoftGPU::MarkDirty(uint32_t addr, uint32_t bytes, SoftGPUVRAMDirty value) {
// Only bother tracking if frameskipping.
if (g_Config.iFrameSkip == 0)
return;
if (!Memory::IsVRAMAddress(addr) || !Memory::IsVRAMAddress(addr + bytes - 1))
return;
if (lastDirtyAddr_ == addr && lastDirtySize_ == bytes && lastDirtyValue_ == value)
return;
uint32_t start = ((addr - PSP_GetVidMemBase()) & 0x001FFFFF) >> 10;
uint32_t end = start + ((bytes + 1023) >> 10);
if (end > sizeof(vramDirty_)) {
end = sizeof(vramDirty_);
}
if (value == SoftGPUVRAMDirty::CLEAR || value == (SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY)) {
memset(vramDirty_ + start, (uint8_t)value, end - start);
} else {
for (uint32_t i = start; i < end; ++i) {
vramDirty_[i] |= (uint8_t)value;
}
}
lastDirtyAddr_ = addr;
lastDirtySize_ = bytes;
lastDirtyValue_ = value;
}
bool SoftGPU::ClearDirty(uint32_t addr, uint32_t stride, uint32_t height, GEBufferFormat fmt, SoftGPUVRAMDirty value) {
uint32_t bytes = height * stride * (fmt == GE_FORMAT_8888 ? 4 : 2);
return ClearDirty(addr, bytes, value);
}
bool SoftGPU::ClearDirty(uint32_t addr, uint32_t bytes, SoftGPUVRAMDirty value) {
if (!Memory::IsVRAMAddress(addr) || !Memory::IsVRAMAddress(addr + bytes - 1))
return false;
uint32_t start = ((addr - PSP_GetVidMemBase()) & 0x001FFFFF) >> 10;
uint32_t end = start + ((bytes + 1023) >> 10);
// Same clamp as MarkDirty - the endpoint checks above accept the whole mirrored 8MB window, so
// bytes can carry us past the end of the array. Without this the loop below writes past it.
if (end > sizeof(vramDirty_)) {
end = sizeof(vramDirty_);
}
bool result = false;
for (uint32_t i = start; i < end; ++i) {
if (vramDirty_[i] & (uint8_t)value) {
result = true;
vramDirty_[i] &= ~(uint8_t)value;
}
}
lastDirtyAddr_ = 0;
lastDirtySize_ = 0;
return result;
}
void SoftGPU::NotifyRenderResized(const DisplayLayoutConfig &config) {
// Force the render params to 480x272 so other things work.
if (config.InternalRotationIsPortrait()) {
PSP_CoreParameter().renderWidth = 272;
PSP_CoreParameter().renderHeight = 480;
} else {
PSP_CoreParameter().renderWidth = 480;
PSP_CoreParameter().renderHeight = 272;
}
}
void SoftGPU::NotifyDisplayResized() {
displayResized_ = true;
}
void SoftGPU::CheckDisplayResized() {
if (displayResized_ && presentation_) {
presentation_->UpdateDisplaySize(PSP_CoreParameter().pixelWidth, PSP_CoreParameter().pixelHeight);
presentation_->UpdateRenderSize(PSP_CoreParameter().renderWidth, PSP_CoreParameter().renderHeight);
displayResized_ = false;
}
}
void SoftGPU::CheckConfigChanged(const DisplayLayoutConfig &config) {
if (configChanged_) {
drawEngineCommon_->NotifyConfigChanged();
BuildReportingInfo();
if (presentation_) {
presentation_->UpdatePostShader(config);
}
configChanged_ = false;
}
}
void SoftGPU::FastRunLoop(DisplayList &list) {
PROFILE_THIS_SCOPE("soft_runloop");
const auto *cmdInfo = softgpuCmdInfo;
int dc = downcount;
SoftDirty dirty = dirtyFlags_;
for (; dc > 0; --dc) {
u32 op = Memory::ReadUnchecked_U32(list.pc);
const u32 cmd = op >> 24;
const auto &info = cmdInfo[cmd];
const u32 diff = op ^ gstate.cmdmem[cmd];
if (diff == 0) {
if (info.flags & FLAG_EXECUTE) {
downcount = dc;
dirtyFlags_ = dirty;
(this->*info.func)(op, diff);
dirty = dirtyFlags_;
dc = downcount;
}
} else {
uint64_t flags = info.flags;
gstate.cmdmem[cmd] = op;
dirty |= SoftDirty(flags >> 8);
if (flags & (FLAG_EXECUTE | FLAG_EXECUTEONCHANGE)) {
downcount = dc;
dirtyFlags_ = dirty;
(this->*info.func)(op, diff);
dirty = dirtyFlags_;
dc = downcount;
}
}
list.pc += 4;
}
downcount = 0;
dirtyFlags_ = dirty;
}
void SoftGPU::ExecuteOp(u32 op, u32 diff) {
const u8 cmd = op >> 24;
const auto info = softgpuCmdInfo[cmd];
if (diff == 0) {
if (info.flags & FLAG_EXECUTE)
(this->*info.func)(op, diff);
} else {
dirtyFlags_ |= SoftDirty(info.flags >> 8);
if (info.flags & (FLAG_EXECUTE | FLAG_EXECUTEONCHANGE))
(this->*info.func)(op, diff);
}
}
void SoftGPU::Execute_BlockTransferStart(u32 op, u32 diff) {
u32 srcBasePtr = gstate.getTransferSrcAddress();
u32 srcStride = gstate.getTransferSrcStride();
u32 dstBasePtr = gstate.getTransferDstAddress();
u32 dstStride = gstate.getTransferDstStride();
int srcX = gstate.getTransferSrcX();
int srcY = gstate.getTransferSrcY();
int dstX = gstate.getTransferDstX();
int dstY = gstate.getTransferDstY();
int width = gstate.getTransferWidth();
int height = gstate.getTransferHeight();
int bpp = gstate.getTransferBpp();
// Use height less one to account for width, which can be greater or less than stride.
const uint32_t src = srcBasePtr + (srcY * srcStride + srcX) * bpp;
const uint32_t srcSize = (height - 1) * (srcStride + width) * bpp;
const uint32_t dst = dstBasePtr + (dstY * dstStride + dstX) * bpp;
const uint32_t dstSize = (height - 1) * (dstStride + width) * bpp;
// Need to flush both source and target, so we overwrite properly.
if (Memory::IsValidRange(src, srcSize) && Memory::IsValidRange(dst, dstSize)) {
drawEngine_->transformUnit.FlushIfOverlap(this, "blockxfer", false, src, srcStride, width * bpp, height);
drawEngine_->transformUnit.FlushIfOverlap(this, "blockxfer", true, dst, dstStride, width * bpp, height);
} else {
drawEngine_->transformUnit.Flush(this, "blockxfer_wrap");
}
DoBlockTransfer(gstate_c.skipDrawReason);
// Could theoretically dirty the framebuffer.
MarkDirty(dst, dstSize, SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY);
}
void SoftGPU::Execute_Prim(u32 op, u32 diff) {
u32 count = op & 0xFFFF;
// Upper bits are ignored.
GEPrimitiveType prim = static_cast<GEPrimitiveType>((op >> 16) & 7);
if (count == 0)
return;
FlushImm();
if (!Memory::IsValidAddress(gstate_c.vertexAddr)) {
ERROR_LOG_REPORT(Log::G3D, "Software: Bad vertex address %08x!", gstate_c.vertexAddr);
return;
}
const void *verts = Memory::GetPointerUnchecked(gstate_c.vertexAddr);
const void *indices = NULL;
const u32 vertType = gstate.vertType;
if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) {
if (!Memory::IsValidAddress(gstate_c.indexAddr)) {
ERROR_LOG_REPORT(Log::G3D, "Software: Bad index address %08x!", gstate_c.indexAddr);
return;
}
indices = Memory::GetPointerUnchecked(gstate_c.indexAddr);
}
cyclesExecuted += EstimatePerVertexCost() * count;
int bytesRead;
drawEngine_->transformUnit.SetDirty(dirtyFlags_);
drawEngine_->transformUnit.SubmitPrimitive(verts, indices, prim, count, vertType, &bytesRead, drawEngine_);
dirtyFlags_ = drawEngine_->transformUnit.GetDirty();
SoftGPUVRAMDirty mark = (gstate_c.skipDrawReason & SKIPDRAW_SKIPFRAME) != 0 ? SoftGPUVRAMDirty::DIRTY : SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY;
MarkDirty(gstate.getFrameBufAddress(), gstate.FrameBufStride(), gstate.getRegionY2() + 1, gstate.FrameBufFormat(), mark);
// After drawing, we advance the vertexAddr (when non indexed) or indexAddr (when indexed).
// Some games rely on this, they don't bother reloading VADDR and IADDR.
// The VADDR/IADDR registers are NOT updated.
gstate_c.AdvanceVerts(vertType, count, bytesRead);
}
void SoftGPU::Execute_Bezier(u32 op, u32 diff) {
// This also make skipping drawing very effective.
if (gstate_c.skipDrawReason & (SKIPDRAW_SKIPFRAME | SKIPDRAW_NON_DISPLAYED_FB)) {
// TODO: Should this eat some cycles? Probably yes. Not sure if important.
return;
}
if (!Memory::IsValidAddress(gstate_c.vertexAddr)) {
ERROR_LOG_REPORT(Log::G3D, "Bad vertex address %08x!", gstate_c.vertexAddr);
return;
}
const void *control_points = Memory::GetPointerUnchecked(gstate_c.vertexAddr);
const void *indices = NULL;
const u32 vertType = gstate.vertType;
if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) {
if (!Memory::IsValidAddress(gstate_c.indexAddr)) {
ERROR_LOG_REPORT(Log::G3D, "Bad index address %08x!", gstate_c.indexAddr);
return;
}
indices = Memory::GetPointerUnchecked(gstate_c.indexAddr);
}
if ((vertType & GE_VTYPE_MORPHCOUNT_MASK) || vertTypeIsSkinningEnabled(vertType)) {
DEBUG_LOG_REPORT(Log::G3D, "Unusual bezier/spline vtype: %08x, morph: %d, bones: %d", vertType, (vertType & GE_VTYPE_MORPHCOUNT_MASK) >> GE_VTYPE_MORPHCOUNT_SHIFT, vertTypeGetNumBoneWeights(vertType));
}
Spline::BezierSurface surface;
surface.tess_u = gstate.getPatchDivisionU();
surface.tess_v = gstate.getPatchDivisionV();
surface.num_points_u = op & 0xFF;
surface.num_points_v = (op >> 8) & 0xFF;
surface.num_patches_u = (surface.num_points_u - 1) / 3;
surface.num_patches_v = (surface.num_points_v - 1) / 3;
surface.primType = gstate.getPatchPrimitiveType();
surface.patchFacing = gstate.patchfacing & 1;
SetDrawType(DRAW_BEZIER, PatchPrimToPrim(surface.primType));
int bytesRead = 0;
drawEngine_->transformUnit.SetDirty(dirtyFlags_);
drawEngineCommon_->SubmitCurve(control_points, indices, surface, gstate.vertType, &bytesRead, "bezier");
dirtyFlags_ = drawEngine_->transformUnit.GetDirty();
SoftGPUVRAMDirty mark = (gstate_c.skipDrawReason & SKIPDRAW_SKIPFRAME) != 0 ? SoftGPUVRAMDirty::DIRTY : SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY;
MarkDirty(gstate.getFrameBufAddress(), gstate.FrameBufStride(), gstate.getRegionY2() + 1, gstate.FrameBufFormat(), mark);
// After drawing, we advance pointers - see SubmitPrim which does the same.
int count = surface.num_points_u * surface.num_points_v;
gstate_c.AdvanceVerts(vertType, count, bytesRead);
}
void SoftGPU::Execute_Spline(u32 op, u32 diff) {
// This also make skipping drawing very effective.
if (gstate_c.skipDrawReason & (SKIPDRAW_SKIPFRAME | SKIPDRAW_NON_DISPLAYED_FB)) {
// TODO: Should this eat some cycles? Probably yes. Not sure if important.
return;
}
if (!Memory::IsValidAddress(gstate_c.vertexAddr)) {
ERROR_LOG_REPORT(Log::G3D, "Bad vertex address %08x!", gstate_c.vertexAddr);
return;
}
const void *control_points = Memory::GetPointerUnchecked(gstate_c.vertexAddr);
const void *indices = NULL;
const u32 vertType = gstate.vertType;
if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) {
if (!Memory::IsValidAddress(gstate_c.indexAddr)) {
ERROR_LOG_REPORT(Log::G3D, "Bad index address %08x!", gstate_c.indexAddr);
return;
}
indices = Memory::GetPointerUnchecked(gstate_c.indexAddr);
}
if ((vertType & GE_VTYPE_MORPHCOUNT_MASK) || vertTypeIsSkinningEnabled(vertType)) {
DEBUG_LOG_REPORT(Log::G3D, "Unusual bezier/spline vtype: %08x, morph: %d, bones: %d", vertType, (vertType & GE_VTYPE_MORPHCOUNT_MASK) >> GE_VTYPE_MORPHCOUNT_SHIFT, vertTypeGetNumBoneWeights(vertType));
}
Spline::SplineSurface surface;
surface.tess_u = gstate.getPatchDivisionU();
surface.tess_v = gstate.getPatchDivisionV();
surface.type_u = (op >> 16) & 0x3;
surface.type_v = (op >> 18) & 0x3;
surface.num_points_u = op & 0xFF;
surface.num_points_v = (op >> 8) & 0xFF;
surface.num_patches_u = surface.num_points_u - 3;
surface.num_patches_v = surface.num_points_v - 3;
surface.primType = gstate.getPatchPrimitiveType();
surface.patchFacing = gstate.patchfacing & 1;
SetDrawType(DRAW_SPLINE, PatchPrimToPrim(surface.primType));
int bytesRead = 0;
drawEngine_->transformUnit.SetDirty(dirtyFlags_);
drawEngineCommon_->SubmitCurve(control_points, indices, surface, vertType, &bytesRead, "spline");
dirtyFlags_ = drawEngine_->transformUnit.GetDirty();
SoftGPUVRAMDirty mark = (gstate_c.skipDrawReason & SKIPDRAW_SKIPFRAME) != 0 ? SoftGPUVRAMDirty::DIRTY : SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY;
MarkDirty(gstate.getFrameBufAddress(), gstate.FrameBufStride(), gstate.getRegionY2() + 1, gstate.FrameBufFormat(), mark);
// After drawing, we advance pointers - see SubmitPrim which does the same.
int count = surface.num_points_u * surface.num_points_v;
gstate_c.AdvanceVerts(vertType, count, bytesRead);
}
void SoftGPU::Execute_LoadClut(u32 op, u32 diff) {
u32 clutAddr = gstate.getClutAddress();
// Avoid the hack in getClutLoadBytes() to inaccurately allow more palette data.
u32 clutTotalBytes = (gstate.getClutLoadBlocks() & 0x3F) * 32;
if (clutTotalBytes > 1024)
clutTotalBytes = 1024;
// Might be copying drawing into the CLUT, so flush.
drawEngine_->transformUnit.FlushIfOverlap(this, "loadclut", false, clutAddr, clutTotalBytes, clutTotalBytes, 1);
bool changed = false;
if (Memory::IsValidAddress(clutAddr)) {
u32 validSize = Memory::ClampValidSizeAt(clutAddr, clutTotalBytes);
changed = memcmp(clut, Memory::GetPointerUnchecked(clutAddr), validSize) != 0;
if (changed)
Memory::MemcpyUnchecked(clut, clutAddr, validSize);
if (validSize < clutTotalBytes) {
// Zero out the parts that were outside valid memory.
memset((u8 *)clut + validSize, 0x00, clutTotalBytes - validSize);
changed = true;
}
} else if (clutAddr != 0) {
// Some invalid addresses trigger a crash, others fill with zero. We always fill zero.
DEBUG_LOG(Log::G3D, "Software: Invalid CLUT address, filling with garbage instead of crashing");
memset(clut, 0x00, clutTotalBytes);
changed = true;
}
if (changed)
drawEngine_->transformUnit.NotifyClutUpdate(clut);
dirtyFlags_ |= SoftDirty::SAMPLER_CLUT;
}
void SoftGPU::Execute_FramebufPtr(u32 op, u32 diff) {
// We assume fb.data won't change while we're drawing.
if (diff) {
drawEngine_->transformUnit.Flush(this, "framebuf");
fb.data = Memory::GetPointerWriteUnchecked(gstate.getFrameBufAddress());
}
}
void SoftGPU::Execute_FramebufFormat(u32 op, u32 diff) {
// We should flush, because ranges within bins may change.
if (diff)
drawEngine_->transformUnit.Flush(this, "framebuf");
}
void SoftGPU::Execute_ZbufPtr(u32 op, u32 diff) {
// We assume depthbuf.data won't change while we're drawing.
if (diff) {
drawEngine_->transformUnit.Flush(this, "depthbuf");
// For the pointer, ignore memory mirrors. This also gives some buffer for draws that go outside.
// TODO: Confirm how wrapping is handled in drawing. Adjust if we ever handle VRAM mirrors more accurately.
depthbuf.data = Memory::GetPointerWriteOrException(gstate.getDepthBufAddress() & 0x041FFFF0);
}
}
void SoftGPU::Execute_VertexType(u32 op, u32 diff) {
if ((diff & GE_VTYPE_THROUGH_MASK) != 0) {
// This affects a lot of things, but some don't matter if it's off - so defer to when it's back on.
dirtyFlags_ |= SoftDirty::RAST_BASIC | SoftDirty::PIXEL_BASIC;
if ((op & GE_VTYPE_THROUGH_MASK) == 0) {
dirtyFlags_ |= SoftDirty::TRANSFORM_MATRIX | SoftDirty::TRANSFORM_VIEWPORT | SoftDirty::TRANSFORM_FOG;
dirtyFlags_ |= SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_0 | SoftDirty::LIGHT_1 | SoftDirty::LIGHT_2 | SoftDirty::LIGHT_3;
dirtyFlags_ |= SoftDirty::PIXEL_CACHED;
}
}
}
void SoftGPU::Execute_WorldMtxNum(u32 op, u32 diff) {
// Setting 0xFFFFF0 will reset to 0.
gstate.worldmtxnum = (GE_CMD_WORLDMATRIXNUMBER << 24) | (op & 0xF);
}
void SoftGPU::Execute_ViewMtxNum(u32 op, u32 diff) {
gstate.viewmtxnum = (GE_CMD_VIEWMATRIXNUMBER << 24) | (op & 0xF);
}
void SoftGPU::Execute_ProjMtxNum(u32 op, u32 diff) {
gstate.projmtxnum = (GE_CMD_PROJMATRIXNUMBER << 24) | (op & 0xF);
}
void SoftGPU::Execute_TgenMtxNum(u32 op, u32 diff) {
gstate.texmtxnum = (GE_CMD_TGENMATRIXNUMBER << 24) | (op & 0xF);
}
void SoftGPU::Execute_BoneMtxNum(u32 op, u32 diff) {
// Setting any bits outside 0x7F are ignored and resets the internal counter.
gstate.boneMatrixNumber = (GE_CMD_BONEMATRIXNUMBER << 24) | (op & 0x7F);
}
void SoftGPU::Execute_WorldMtxData(u32 op, u32 diff) {
int num = gstate.worldmtxnum & 0x00FFFFFF;
if (num < 12) {
u32 *target = (u32 *)&gstate.worldMatrix[num];
u32 newVal = op << 8;
if (newVal != *target) {
*target = newVal;
dirtyFlags_ |= SoftDirty::TRANSFORM_MATRIX;
gstate_c.Dirty(DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
}
// Also update the CPU visible values, which update differently.
u32 *target = &matrixVisible.all[12 * 8 + (num & 0xF)];
*target = op & 0x00FFFFFF;
num++;
gstate.worldmtxnum = (GE_CMD_WORLDMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.worldmtxdata = GE_CMD_WORLDMATRIXDATA << 24;
}
void SoftGPU::Execute_ViewMtxData(u32 op, u32 diff) {
int num = gstate.viewmtxnum & 0x00FFFFFF;
if (num < 12) {
u32 *target = (u32 *)&gstate.viewMatrix[num];
u32 newVal = op << 8;
if (newVal != *target) {
*target = newVal;
dirtyFlags_ |= SoftDirty::TRANSFORM_MATRIX;
gstate_c.Dirty(DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
}
// Also update the CPU visible values, which update differently.
u32 *target = &matrixVisible.all[12 * 8 + 12 + (num & 0xF)];
*target = op & 0x00FFFFFF;
num++;
gstate.viewmtxnum = (GE_CMD_VIEWMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.viewmtxdata = GE_CMD_VIEWMATRIXDATA << 24;
}
void SoftGPU::Execute_ProjMtxData(u32 op, u32 diff) {
int num = gstate.projmtxnum & 0x00FFFFFF;
if (num < 16) {
u32 *target = (u32 *)&gstate.projMatrix[num];
u32 newVal = op << 8;
if (newVal != *target) {
*target = newVal;
dirtyFlags_ |= SoftDirty::TRANSFORM_MATRIX;
gstate_c.Dirty(DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_VIEW_PROJ_MATRIX);
}
}
// Also update the CPU visible values, which update differently.
u32 *target = &matrixVisible.all[12 * 8 + 12 + 12 + (num & 0xF)];
*target = op & 0x00FFFFFF;
num++;
gstate.projmtxnum = (GE_CMD_PROJMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.projmtxdata = GE_CMD_PROJMATRIXDATA << 24;
}
void SoftGPU::Execute_TgenMtxData(u32 op, u32 diff) {
int num = gstate.texmtxnum & 0x00FFFFFF;
if (num < 12) {
u32 *target = (u32 *)&gstate.tgenMatrix[num];
u32 newVal = op << 8;
if (newVal != *target) {
*target = newVal;
// This is mainly used in vertex read, but also affects if we enable texture projection.
dirtyFlags_ |= SoftDirty::RAST_TEX;
}
}
// Doesn't wrap to any other matrix.
if ((num & 0xF) < 12) {
matrixVisible.tgen[num & 0xF] = op & 0x00FFFFFF;
}
num++;
gstate.texmtxnum = (GE_CMD_TGENMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.texmtxdata = GE_CMD_TGENMATRIXDATA << 24;
}
void SoftGPU::Execute_BoneMtxData(u32 op, u32 diff) {
int num = gstate.boneMatrixNumber & 0x00FFFFFF;
if (num < 96) {
u32 *target = (u32 *)&gstate.boneMatrix[num];
u32 newVal = op << 8;
// No dirtying, we read bone data during vertex read.
*target = newVal;
}
// Also update the CPU visible values, which update differently.
u32 *target = &matrixVisible.all[(num & 0x7F)];
*target = op & 0x00FFFFFF;
num++;
gstate.boneMatrixNumber = (GE_CMD_BONEMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.boneMatrixData = GE_CMD_BONEMATRIXDATA << 24;
}
static void CopyMatrix24(u32_le *result, const u32 *mtx, u32 count, u32 cmdbits) {
for (u32 i = 0; i < count; ++i) {
result[i] = mtx[i] | cmdbits;
}
}
bool SoftGPU::GetMatrix24(GEMatrixType type, u32_le *result, u32 cmdbits) {
switch (type) {
case GE_MTX_BONE0:
case GE_MTX_BONE1:
case GE_MTX_BONE2:
case GE_MTX_BONE3:
case GE_MTX_BONE4:
case GE_MTX_BONE5:
case GE_MTX_BONE6:
case GE_MTX_BONE7:
CopyMatrix24(result, matrixVisible.bone + (type - GE_MTX_BONE0) * 12, 12, cmdbits);
break;
case GE_MTX_TEXGEN:
CopyMatrix24(result, matrixVisible.tgen, 12, cmdbits);
break;
case GE_MTX_WORLD:
CopyMatrix24(result, matrixVisible.world, 12, cmdbits);
break;
case GE_MTX_VIEW:
CopyMatrix24(result, matrixVisible.view, 12, cmdbits);
break;
case GE_MTX_PROJECTION:
CopyMatrix24(result, matrixVisible.proj, 16, cmdbits);
break;
default:
return false;
}
return true;
}
void SoftGPU::ResetMatrices() {
GPUCommon::ResetMatrices();
dirtyFlags_ |= SoftDirty::TRANSFORM_MATRIX | SoftDirty::RAST_TEX;
}
void SoftGPU::Execute_ImmVertexAlphaPrim(u32 op, u32 diff) {
GPUCommon::Execute_ImmVertexAlphaPrim(op, diff);
// We won't flush as often as hardware renderers, so we want to flush right away.
FlushImm();
}
void SoftGPU::Execute_Call(u32 op, u32 diff) {
PROFILE_THIS_SCOPE("gpu_call");
const u32 target = gstate_c.getRelativeAddress(op & 0x00FFFFFC);
if (!Memory::IsValidAddress(target)) {
ERROR_LOG(Log::G3D, "CALL to illegal address %08x - ignoring! data=%06x", target, op & 0x00FFFFFF);
if (g_Config.bIgnoreBadMemAccess) {
return;
}
gpuState = GPUSTATE_ERROR;
downcount = 0;
return;
}
const u32 retval = currentList->pc + 4;
if (currentList->stackptr == ARRAY_SIZE(currentList->stack)) {
ERROR_LOG(Log::G3D, "CALL: Stack full!");
} else {
auto &stackEntry = currentList->stack[currentList->stackptr++];
stackEntry.pc = retval;
stackEntry.offsetAddr = gstate_c.offsetAddr;
// The base address is NOT saved/restored for a regular call.
UpdatePC(currentList->pc, target - 4);
currentList->pc = target - 4; // pc will be increased after we return, counteract that
}
}
void SoftGPU::FinishDeferred() {
// Need to flush before going back to CPU, so drawing is appropriately visible.
drawEngine_->transformUnit.Flush(this, "finish");
}
int SoftGPU::ListSync(int listid, int mode) {
// Take this as a cue that we need to finish drawing.
drawEngine_->transformUnit.Flush(this, "listsync");
return GPUCommon::ListSync(listid, mode);
}
u32 SoftGPU::DrawSync(int mode) {
// Take this as a cue that we need to finish drawing.
drawEngine_->transformUnit.Flush(this, "drawsync");
return GPUCommon::DrawSync(mode);
}
void SoftGPU::GetStats(StringWriter &w) {
drawEngine_->transformUnit.GetStats(w);
}
void SoftGPU::InvalidateCache(u32 addr, int size, GPUInvalidationType type)
{
// Nothing to invalidate.
}
void SoftGPU::PerformWriteFormattedFromMemory(u32 addr, int size, int width, GEBufferFormat format)
{
// Ignore.
}
bool SoftGPU::PerformMemoryCopy(u32 dest, u32 src, int size, GPUCopyFlag flags) {
// Nothing to update.
InvalidateCache(dest, size, GPU_INVALIDATE_HINT);
if (!(flags & GPUCopyFlag::DEBUG_NOTIFIED))
recorder_.NotifyMemcpy(dest, src, size);
// Let's just be safe.
MarkDirty(dest, size, SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY);
return false;
}
bool SoftGPU::PerformMemorySet(u32 dest, u8 v, int size)
{
// Nothing to update.
InvalidateCache(dest, size, GPU_INVALIDATE_HINT);
recorder_.NotifyMemset(dest, v, size);
// Let's just be safe.
MarkDirty(dest, size, SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY);
return false;
}
bool SoftGPU::PerformReadbackToMemory(u32 dest, int size)
{
// Nothing to update.
InvalidateCache(dest, size, GPU_INVALIDATE_HINT);
return false;
}
bool SoftGPU::PerformWriteColorFromMemory(u32 dest, int size)
{
// Nothing to update.
InvalidateCache(dest, size, GPU_INVALIDATE_HINT);
recorder_.NotifyUpload(dest, size);
return false;
}
bool SoftGPU::PerformWriteStencilFromMemory(u32 dest, int size, WriteStencil flags)
{
return false;
}
bool SoftGPU::FramebufferDirty() {
if (g_Config.iFrameSkip != 0) {
return ClearDirty(displayFramebuf_, displayStride_, 272, displayFormat_, SoftGPUVRAMDirty::DIRTY);
}
return true;
}
bool SoftGPU::FramebufferReallyDirty() {
if (g_Config.iFrameSkip != 0) {
return ClearDirty(displayFramebuf_, displayStride_, 272, displayFormat_, SoftGPUVRAMDirty::REALLY_DIRTY);
}
return true;
}
static DrawingCoords GetTargetSize(int stride) {
int w = std::min(stride, std::max(gstate.getRegionX2(), gstate.getScissorX2()) + 1);
int h = std::max(gstate.getRegionY2(), gstate.getScissorY2()) + 1;
if (gstate.getRegionX2() == 1023 && gstate.getRegionY2() == 1023) {
// Some games max out region, but always scissor to an appropriate size.
// Both values always scissor, we just prefer region as it's usually a more stable size.
w = std::max(stride, gstate.getScissorX2() + 1);
h = std::max(272, gstate.getScissorY2() + 1);
}
return DrawingCoords((s16)w, (s16)h);
}
bool SoftGPU::GetCurrentFramebuffer(GPUDebugBuffer &buffer, GPUDebugFramebufferType type, int maxRes) {
int stride = gstate.FrameBufStride();
DrawingCoords size = GetTargetSize(stride);
GEBufferFormat fmt = gstate.FrameBufFormat();
const u8 *src = fb.data;
if (!Memory::IsValidAddress(displayFramebuf_))
return false;
if (type == GPU_DBG_FRAMEBUF_DISPLAY) {
size.x = 480;
size.y = 272;
stride = displayStride_;
fmt = displayFormat_;
src = Memory::GetPointerOrException(displayFramebuf_);
}
buffer.Allocate(size.x, size.y, fmt);
const int depth = fmt == GE_FORMAT_8888 ? 4 : 2;
u8 *dst = buffer.GetData();
const int byteWidth = size.x * depth;
for (int16_t y = 0; y < size.y; ++y) {
memcpy(dst, src, byteWidth);
dst += byteWidth;
src += stride * depth;
}
return true;
}
bool SoftGPU::GetOutputFramebuffer(GPUDebugBuffer &buffer) {
return GetCurrentFramebuffer(buffer, GPU_DBG_FRAMEBUF_DISPLAY, 1);
}
bool SoftGPU::GetCurrentDepthbuffer(GPUDebugBuffer &buffer) {
DrawingCoords size = GetTargetSize(gstate.DepthBufStride());
buffer.Allocate(size.x, size.y, GPU_DBG_FORMAT_16BIT);
const int depth = 2;
const u8 *src = depthbuf.data;
u8 *dst = buffer.GetData();
for (int16_t y = 0; y < size.y; ++y) {
memcpy(dst, src, size.x * depth);
dst += size.x * depth;
src += gstate.DepthBufStride() * depth;
}
return true;
}
static inline u8 GetPixelStencil(GEBufferFormat fmt, int fbStride, int x, int y) {
if (fmt == GE_FORMAT_565) {
// Always treated as 0 for comparison purposes.
return 0;
} else if (fmt == GE_FORMAT_5551) {
return ((fb.Get16(x, y, fbStride) & 0x8000) != 0) ? 0xFF : 0;
} else if (fmt == GE_FORMAT_4444) {
return Convert4To8(fb.Get16(x, y, fbStride) >> 12);
} else {
return fb.Get32(x, y, fbStride) >> 24;
}
}
bool SoftGPU::GetCurrentStencilbuffer(GPUDebugBuffer &buffer) {
DrawingCoords size = GetTargetSize(gstate.FrameBufStride());
buffer.Allocate(size.x, size.y, GPU_DBG_FORMAT_8BIT);
u8 *row = buffer.GetData();
for (int16_t y = 0; y < size.y; ++y) {
for (int16_t x = 0; x < size.x; ++x) {
row[x] = GetPixelStencil(gstate.FrameBufFormat(), gstate.FrameBufStride(), x, y);
}
row += size.x;
}
return true;
}
bool SoftGPU::GetCurrentTexture(GPUDebugBuffer &buffer, int level, bool *isFramebuffer) {
*isFramebuffer = false;
return Rasterizer::GetCurrentTexture(buffer, level);
}
bool SoftGPU::GetCurrentClut(GPUDebugBuffer &buffer) {
const u32 bpp = gstate.getClutPaletteFormat() == GE_CMODE_32BIT_ABGR8888 ? 4 : 2;
const u32 pixels = 1024 / bpp;
buffer.Allocate(pixels, 1, (GEBufferFormat)gstate.getClutPaletteFormat());
memcpy(buffer.GetData(), clut, 1024);
return true;
}
bool SoftGPU::DescribeCodePtr(const u8 *ptr, std::string &name) {
std::string subname;
if (Sampler::DescribeCodePtr(ptr, subname)) {
name = "SamplerJit:" + subname;
return true;
}
if (Rasterizer::DescribeCodePtr(ptr, subname)) {
name = "RasterizerJit:" + subname;
return true;
}
return GPUCommon::DescribeCodePtr(ptr, name);
}