Merge pull request #22593 from hrydgard/null-gpu-minimal

GPU: Add a null GPU (headless --graphics=null), and skip discarded AA lines like culled draws
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-10-16 09:29:49 +02:00
commit ea8b92ebb7
20 files changed
+651 -287

No files matched your search

+7
View File
@@ -344,6 +344,9 @@ int CommandLineOptions::PrintUsage(const char *progname, const char *situationTe
PRINT_STDOUT(" --graphics=BACKEND use a different gpu backend\n");
PRINT_STDOUT(" options: gles, software, etc. (also opengl3.1, etc.)\n");
if (mode == CmdLineMode::Headless) {
PRINT_STDOUT(" and null, which draws nothing\n");
}
return 0;
}
@@ -466,6 +469,10 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[], C
} else if (restOfOption == "software") {
gpuBackend = GPUBackend::OPENGL;
softwareRendering = true;
} else if (restOfOption == "null" && mode == CmdLineMode::Headless) {
// Runs display lists but draws nothing, for measuring the CPU emulation.
nullGraphics = true;
softwareRendering = false;
} else if (sscanf(restOfOption.c_str(), "gles%lg", &glVersionTemp) == 1 || sscanf(restOfOption.c_str(), "opengl%lg", &glVersionTemp) == 1) {
gpuBackend = GPUBackend::OPENGL;
softwareRendering = false;
+2
View File
@@ -33,6 +33,8 @@ struct CommandLineOptions {
std::optional<bool> fullscreen;
std::optional<GPUBackend> gpuBackend;
std::optional<bool> softwareRendering;
// --graphics=null: headless only, see GPU_Null.h.
std::optional<bool> nullGraphics;
std::optional<bool> enableLogging;
std::optional<LogLevel> logLevel; // Override log level with this.
std::optional<std::string> log;
+1
View File
@@ -28,6 +28,7 @@ enum GPUCore : int {
GPUCORE_SOFTWARE = 1,
GPUCORE_DIRECTX11 = 3,
GPUCORE_VULKAN = 4,
GPUCORE_NULL = 5, // Draws nothing, see GPU_Null.h.
};
enum class FPSLimit {
+1 -1
View File
@@ -636,7 +636,7 @@ static bool CPU_Init(FileLoader *fileLoader, IdentifiedFileType type, std::strin
// Update things that depend on game-specific config here.
// Compat settings can override the software renderer, take care of that here.
if (g_Config.bSoftwareRendering || g_CoreParameter.compat.flags().ForceSoftwareRenderer) {
if ((g_Config.bSoftwareRendering || g_CoreParameter.compat.flags().ForceSoftwareRenderer) && g_CoreParameter.gpuCore != GPUCORE_NULL) {
g_CoreParameter.gpuCore = GPUCORE_SOFTWARE;
}
+2
View File
@@ -189,6 +189,8 @@ set(GPU_SOURCES
Software/Sampler.h
Software/SoftGpu.cpp
Software/SoftGpu.h
Null/GPU_Null.cpp
Null/GPU_Null.h
Software/TransformUnit.cpp
Software/TransformUnit.h
)
+4 -1
View File
@@ -31,6 +31,7 @@
#endif
#include "GPU/Vulkan/GPU_Vulkan.h"
#include "GPU/Software/SoftGpu.h"
#include "GPU/Null/GPU_Null.h"
#if PPSSPP_API(D3D11)
#include "GPU/D3D11/GPU_D3D11.h"
@@ -69,13 +70,15 @@ static GPUCommon *CreateGPUCore(GPUCore gpuCore, GraphicsContext *ctx, Draw::Dra
}
return new GPU_Vulkan(ctx, draw);
#endif
case GPUCORE_NULL:
return CreateNullGPU(ctx);
default:
return nullptr;
}
}
bool GPU_Init(GPUCore gpuCore, GraphicsContext *ctx, Draw::DrawContext *draw) {
_dbg_assert_(draw || gpuCore == GPUCORE_SOFTWARE);
_dbg_assert_(draw || gpuCore == GPUCORE_SOFTWARE || gpuCore == GPUCORE_NULL);
_dbg_assert_(!gpu);
GPUCommon *createdGPU = CreateGPUCore(gpuCore, ctx, draw);
+2
View File
@@ -347,6 +347,7 @@
<ClInclude Include="Software\RasterizerTypes.h" />
<ClInclude Include="Software\Sampler.h" />
<ClInclude Include="Software\SoftGpu.h" />
<ClInclude Include="Null\GPU_Null.h" />
<ClInclude Include="Software\TransformUnit.h" />
<ClInclude Include="Software\GEMath.h" />
<ClInclude Include="Common\TextureDecoder.h" />
@@ -471,6 +472,7 @@
<ClCompile Include="Software\RasterizerRectangle.cpp" />
<ClCompile Include="Software\Sampler.cpp" />
<ClCompile Include="Software\SoftGpu.cpp" />
<ClCompile Include="Null\GPU_Null.cpp" />
<ClCompile Include="Software\TransformUnit.cpp" />
<ClCompile Include="Software\GEMath.cpp" />
<ClCompile Include="Common\TextureDecoder.cpp" />
+9
View File
@@ -4,6 +4,9 @@
<Filter Include="GLES">
<UniqueIdentifier>{f7563dba-8146-4c21-a092-e864ff145d79}</UniqueIdentifier>
</Filter>
<Filter Include="Null">
<UniqueIdentifier>{9879c213-d775-44fc-b1e4-a9d19ac2dff5}</UniqueIdentifier>
</Filter>
<Filter Include="Software">
<UniqueIdentifier>{4f6d1284-2c23-4ebc-842c-666a1305bfed}</UniqueIdentifier>
</Filter>
@@ -48,6 +51,9 @@
<ClInclude Include="Software\SoftGpu.h">
<Filter>Software</Filter>
</ClInclude>
<ClInclude Include="Null\GPU_Null.h">
<Filter>Null</Filter>
</ClInclude>
<ClInclude Include="Software\TransformUnit.h">
<Filter>Software</Filter>
</ClInclude>
@@ -299,6 +305,9 @@
<ClCompile Include="Software\SoftGpu.cpp">
<Filter>Software</Filter>
</ClCompile>
<ClCompile Include="Null\GPU_Null.cpp">
<Filter>Null</Filter>
</ClCompile>
<ClCompile Include="Software\TransformUnit.cpp">
<Filter>Software</Filter>
</ClCompile>
+263
View File
@@ -1629,6 +1629,269 @@ void GPUCommon::FastLoadBoneMatrix(u32 target) {
}
}
void GPUCommon::Execute_WorldMtxNum(u32 op, u32 diff) {
if (!currentList) {
gstate.worldmtxnum = (GE_CMD_WORLDMATRIXNUMBER << 24) | (op & 0xF);
return;
}
// This is almost always followed by GE_CMD_WORLDMATRIXDATA.
const u32_le *src = (const u32_le *)Memory::GetPointerUnchecked(currentList->pc + 4);
u32 *dst = (u32 *)(gstate.worldMatrix + (op & 0xF));
const int end = 12 - (op & 0xF);
int i = 0;
// We must record the individual data commands while debugRecording_.
bool fastLoad = !debugRecording_ && end > 0;
// Stalling in the middle of a matrix would be stupid, I doubt this check is necessary.
if (currentList->pc < currentList->stall && currentList->pc + end * 4 >= currentList->stall) {
fastLoad = false;
}
if (fastLoad) {
while ((src[i] >> 24) == GE_CMD_WORLDMATRIXDATA) {
const u32 newVal = src[i] << 8;
if (dst[i] != newVal) {
Flush();
dst[i] = newVal;
gstate_c.Dirty(DIRTY_WORLDMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
if (++i >= end) {
break;
}
}
}
const int count = i;
gstate.worldmtxnum = (GE_CMD_WORLDMATRIXNUMBER << 24) | ((op & 0xF) + count);
// Skip over the loaded data, it's done now.
currentList->pc += count * 4;
UpdatePC(currentList->pc, currentList->pc);
}
void GPUCommon::Execute_WorldMtxData(u32 op, u32 diff) {
// Note: it's uncommon to get here now, see above.
int num = gstate.worldmtxnum & 0x00FFFFFF;
u32 newVal = op << 8;
if (num < 12 && newVal != ((const u32 *)gstate.worldMatrix)[num]) {
Flush();
((u32 *)gstate.worldMatrix)[num] = newVal;
gstate_c.Dirty(DIRTY_WORLDMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
num++;
gstate.worldmtxnum = (GE_CMD_WORLDMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.worldmtxdata = GE_CMD_WORLDMATRIXDATA << 24;
}
void GPUCommon::Execute_ViewMtxNum(u32 op, u32 diff) {
if (!currentList) {
gstate.viewmtxnum = (GE_CMD_VIEWMATRIXNUMBER << 24) | (op & 0xF);
return;
}
// This is almost always followed by GE_CMD_VIEWMATRIXDATA.
const u32_le *src = (const u32_le *)Memory::GetPointerUnchecked(currentList->pc + 4);
u32 *dst = (u32 *)(gstate.viewMatrix + (op & 0xF));
const int end = 12 - (op & 0xF);
int i = 0;
bool fastLoad = !debugRecording_ && end > 0;
if (currentList->pc < currentList->stall && currentList->pc + end * 4 >= currentList->stall) {
fastLoad = false;
}
if (fastLoad) {
while ((src[i] >> 24) == GE_CMD_VIEWMATRIXDATA) {
const u32 newVal = src[i] << 8;
if (dst[i] != newVal) {
Flush();
dst[i] = newVal;
gstate_c.Dirty(DIRTY_VIEWMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX) ;
}
if (++i >= end) {
break;
}
}
}
const int count = i;
gstate.viewmtxnum = (GE_CMD_VIEWMATRIXNUMBER << 24) | ((op & 0xF) + count);
// Skip over the loaded data, it's done now.
currentList->pc += count * 4;
UpdatePC(currentList->pc, currentList->pc);
}
void GPUCommon::Execute_ViewMtxData(u32 op, u32 diff) {
// Note: it's uncommon to get here now, see above.
int num = gstate.viewmtxnum & 0x00FFFFFF;
u32 newVal = op << 8;
if (num < 12 && newVal != ((const u32 *)gstate.viewMatrix)[num]) {
Flush();
((u32 *)gstate.viewMatrix)[num] = newVal;
gstate_c.Dirty(DIRTY_VIEWMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
num++;
gstate.viewmtxnum = (GE_CMD_VIEWMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.viewmtxdata = GE_CMD_VIEWMATRIXDATA << 24;
}
void GPUCommon::Execute_ProjMtxNum(u32 op, u32 diff) {
if (!currentList) {
gstate.projmtxnum = (GE_CMD_PROJMATRIXNUMBER << 24) | (op & 0xF);
return;
}
// This is almost always followed by GE_CMD_PROJMATRIXDATA.
const u32_le *src = (const u32_le *)Memory::GetPointerUnchecked(currentList->pc + 4);
u32 *dst = (u32 *)(gstate.projMatrix + (op & 0xF));
const int end = 16 - (op & 0xF);
int i = 0;
bool fastLoad = !debugRecording_;
if (currentList->pc < currentList->stall && currentList->pc + end * 4 >= currentList->stall) {
fastLoad = false;
}
if (fastLoad) {
while ((src[i] >> 24) == GE_CMD_PROJMATRIXDATA) {
const u32 newVal = src[i] << 8;
if (dst[i] != newVal) {
Flush();
dst[i] = newVal;
gstate_c.Dirty(DIRTY_PROJMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
if (++i >= end) {
break;
}
}
}
const int count = i;
gstate.projmtxnum = (GE_CMD_PROJMATRIXNUMBER << 24) | ((op & 0xF) + count);
// Skip over the loaded data, it's done now.
currentList->pc += count * 4;
UpdatePC(currentList->pc, currentList->pc);
}
void GPUCommon::Execute_ProjMtxData(u32 op, u32 diff) {
// Note: it's uncommon to get here now, see above.
int num = gstate.projmtxnum & 0x00FFFFFF;
u32 newVal = op << 8;
if (num < 16 && newVal != ((const u32 *)gstate.projMatrix)[num]) {
Flush();
((u32 *)gstate.projMatrix)[num] = newVal;
gstate_c.Dirty(DIRTY_PROJMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
num++;
if (num <= 16)
gstate.projmtxnum = (GE_CMD_PROJMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.projmtxdata = GE_CMD_PROJMATRIXDATA << 24;
}
void GPUCommon::Execute_TgenMtxNum(u32 op, u32 diff) {
if (!currentList) {
gstate.texmtxnum = (GE_CMD_TGENMATRIXNUMBER << 24) | (op & 0xF);
return;
}
// This is almost always followed by GE_CMD_TGENMATRIXDATA.
const u32_le *src = (const u32_le *)Memory::GetPointerUnchecked(currentList->pc + 4);
u32 *dst = (u32 *)(gstate.tgenMatrix + (op & 0xF));
const int end = 12 - (op & 0xF);
int i = 0;
bool fastLoad = !debugRecording_ && end > 0;
if (currentList->pc < currentList->stall && currentList->pc + end * 4 >= currentList->stall) {
fastLoad = false;
}
if (fastLoad) {
while ((src[i] >> 24) == GE_CMD_TGENMATRIXDATA) {
const u32 newVal = src[i] << 8;
if (dst[i] != newVal) {
Flush();
dst[i] = newVal;
// We check the matrix to see if we need projection.
gstate_c.Dirty(DIRTY_TEXMATRIX | DIRTY_FRAGMENTSHADER_STATE);
}
if (++i >= end) {
break;
}
}
}
const int count = i;
gstate.texmtxnum = (GE_CMD_TGENMATRIXNUMBER << 24) | ((op & 0xF) + count);
// Skip over the loaded data, it's done now.
currentList->pc += count * 4;
UpdatePC(currentList->pc, currentList->pc);
}
void GPUCommon::Execute_TgenMtxData(u32 op, u32 diff) {
// Note: it's uncommon to get here now, see above.
int num = gstate.texmtxnum & 0x00FFFFFF;
u32 newVal = op << 8;
if (num < 12 && newVal != ((const u32 *)gstate.tgenMatrix)[num]) {
Flush();
((u32 *)gstate.tgenMatrix)[num] = newVal;
gstate_c.Dirty(DIRTY_TEXMATRIX | DIRTY_FRAGMENTSHADER_STATE); // We check the matrix to see if we need projection
}
num++;
gstate.texmtxnum = (GE_CMD_TGENMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.texmtxdata = GE_CMD_TGENMATRIXDATA << 24;
}
void GPUCommon::Execute_BoneMtxNum(u32 op, u32 diff) {
if (!currentList) {
gstate.boneMatrixNumber = (GE_CMD_BONEMATRIXNUMBER << 24) | (op & 0x7F);
return;
}
// This is almost always followed by GE_CMD_BONEMATRIXDATA.
const u32_le *src = (const u32_le *)Memory::GetPointerUnchecked(currentList->pc + 4);
u32 *dst = (u32 *)(gstate.boneMatrix + (op & 0x7F));
const int end = 12 * 8 - (op & 0x7F);
int i = 0;
bool fastLoad = !debugRecording_ && end > 0;
if (currentList->pc < currentList->stall && currentList->pc + end * 4 >= currentList->stall) {
fastLoad = false;
}
if (fastLoad) {
while ((src[i] >> 24) == GE_CMD_BONEMATRIXDATA) {
dst[i] = src[i] << 8;
if (++i >= end) {
break;
}
}
}
const int count = i;
gstate.boneMatrixNumber = (GE_CMD_BONEMATRIXNUMBER << 24) | ((op & 0x7F) + count);
// Skip over the loaded data, it's done now.
currentList->pc += count * 4;
UpdatePC(currentList->pc, currentList->pc);
}
void GPUCommon::Execute_BoneMtxData(u32 op, u32 diff) {
// Note: it's uncommon to get here now, see above.
int num = gstate.boneMatrixNumber & 0x00FFFFFF;
u32 newVal = op << 8;
if (num < 96 && newVal != ((const u32 *)gstate.boneMatrix)[num]) {
// Bone matrices should NOT flush, as we're always doing skinning in decode nowadays!
((u32 *)gstate.boneMatrix)[num] = newVal;
}
num++;
gstate.boneMatrixNumber = (GE_CMD_BONEMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.boneMatrixData = GE_CMD_BONEMATRIXDATA << 24;
}
struct DisplayList_v1 {
int id;
u32 startpc;
+12
View File
@@ -194,6 +194,18 @@ public:
void Execute_ImmVertexAlphaPrim(u32 op, u32 diff);
// Matrix loads, with a fast path for the DATA commands following a NUM. SoftGPU has its own.
void Execute_WorldMtxNum(u32 op, u32 diff);
void Execute_WorldMtxData(u32 op, u32 diff);
void Execute_ViewMtxNum(u32 op, u32 diff);
void Execute_ViewMtxData(u32 op, u32 diff);
void Execute_ProjMtxNum(u32 op, u32 diff);
void Execute_ProjMtxData(u32 op, u32 diff);
void Execute_TgenMtxNum(u32 op, u32 diff);
void Execute_TgenMtxData(u32 op, u32 diff);
void Execute_BoneMtxNum(u32 op, u32 diff);
void Execute_BoneMtxData(u32 op, u32 diff);
void Execute_Unknown(u32 op, u32 diff);
static int EstimatePerVertexCost();
+7 -272
View File
@@ -878,12 +878,10 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) {
SetDrawType(DRAW_PRIM, prim);
// Discard AA lines as we can't do anything that makes sense with these anyway. The SW plugin might, though.
if (gstate.isAntiAliasEnabled()) {
// Heuristic derived from discussions in #6483 and #12588.
// Discard AA lines in Persona 3 Portable, DOA Paradise and Summon Night 5, while still keeping AA lines in Echochrome.
if ((prim == GE_PRIM_LINE_STRIP || prim == GE_PRIM_LINES) && gstate.getTextureFunction() == GE_TEXFUNC_REPLACE)
return;
}
// Heuristic derived from discussions in #6483 and #12588.
// Discard AA lines in Persona 3 Portable, DOA Paradise and Summon Night 5, while still keeping AA lines in Echochrome.
// They're skipped like culled draws, so the pointers still move past them and the time is still taken.
const bool discardLines = gstate.isAntiAliasEnabled() && (prim == GE_PRIM_LINE_STRIP || prim == GE_PRIM_LINES) && gstate.getTextureFunction() == GE_TEXFUNC_REPLACE;
// Update cached framebuffer format.
// We store it in the cache so it can be modified for blue-to-alpha, next.
@@ -1012,11 +1010,11 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) {
#endif
// If certain conditions are true, do frustum culling.
bool passCulling = PASSES_CULLING;
bool passCulling = PASSES_CULLING && !discardLines;
if (passCulling && !(flags & ClipInfoFlags::Valid)) {
flags = DrawEngineCommon::UncheckedClipInfoFlags();
}
if (!passCulling) {
if (!passCulling && !discardLines) {
// Do software culling.
if (drawEngineCommon_->TestBoundingBoxFast(gstate_c.cullMatrix, verts, inds, count, decoder, vertexType, &flags)) {
passCulling = true;
@@ -1068,7 +1066,7 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) {
case GE_CMD_PRIM:
{
GEPrimitiveType newPrim = static_cast<GEPrimitiveType>((data >> 16) & 7);
if (IsTrianglePrim(newPrim) != isTriangle)
if (IsTrianglePrim(newPrim) != isTriangle || discardLines)
goto bail; // Can't join over this boundary. Might as well exit and get this on the next time around.
// TODO: more efficient updating of verts/inds
@@ -1471,269 +1469,6 @@ void GPUCommonHW::Execute_LoadClut(u32 op, u32 diff) {
textureCache_->LoadClut(gstate.getClutAddress(), gstate.getClutLoadBytes(), &recorder_);
}
void GPUCommonHW::Execute_WorldMtxNum(u32 op, u32 diff) {
if (!currentList) {
gstate.worldmtxnum = (GE_CMD_WORLDMATRIXNUMBER << 24) | (op & 0xF);
return;
}
// This is almost always followed by GE_CMD_WORLDMATRIXDATA.
const u32_le *src = (const u32_le *)Memory::GetPointerUnchecked(currentList->pc + 4);
u32 *dst = (u32 *)(gstate.worldMatrix + (op & 0xF));
const int end = 12 - (op & 0xF);
int i = 0;
// We must record the individual data commands while debugRecording_.
bool fastLoad = !debugRecording_ && end > 0;
// Stalling in the middle of a matrix would be stupid, I doubt this check is necessary.
if (currentList->pc < currentList->stall && currentList->pc + end * 4 >= currentList->stall) {
fastLoad = false;
}
if (fastLoad) {
while ((src[i] >> 24) == GE_CMD_WORLDMATRIXDATA) {
const u32 newVal = src[i] << 8;
if (dst[i] != newVal) {
Flush();
dst[i] = newVal;
gstate_c.Dirty(DIRTY_WORLDMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
if (++i >= end) {
break;
}
}
}
const int count = i;
gstate.worldmtxnum = (GE_CMD_WORLDMATRIXNUMBER << 24) | ((op & 0xF) + count);
// Skip over the loaded data, it's done now.
currentList->pc += count * 4;
UpdatePC(currentList->pc, currentList->pc);
}
void GPUCommonHW::Execute_WorldMtxData(u32 op, u32 diff) {
// Note: it's uncommon to get here now, see above.
int num = gstate.worldmtxnum & 0x00FFFFFF;
u32 newVal = op << 8;
if (num < 12 && newVal != ((const u32 *)gstate.worldMatrix)[num]) {
Flush();
((u32 *)gstate.worldMatrix)[num] = newVal;
gstate_c.Dirty(DIRTY_WORLDMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
num++;
gstate.worldmtxnum = (GE_CMD_WORLDMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.worldmtxdata = GE_CMD_WORLDMATRIXDATA << 24;
}
void GPUCommonHW::Execute_ViewMtxNum(u32 op, u32 diff) {
if (!currentList) {
gstate.viewmtxnum = (GE_CMD_VIEWMATRIXNUMBER << 24) | (op & 0xF);
return;
}
// This is almost always followed by GE_CMD_VIEWMATRIXDATA.
const u32_le *src = (const u32_le *)Memory::GetPointerUnchecked(currentList->pc + 4);
u32 *dst = (u32 *)(gstate.viewMatrix + (op & 0xF));
const int end = 12 - (op & 0xF);
int i = 0;
bool fastLoad = !debugRecording_ && end > 0;
if (currentList->pc < currentList->stall && currentList->pc + end * 4 >= currentList->stall) {
fastLoad = false;
}
if (fastLoad) {
while ((src[i] >> 24) == GE_CMD_VIEWMATRIXDATA) {
const u32 newVal = src[i] << 8;
if (dst[i] != newVal) {
Flush();
dst[i] = newVal;
gstate_c.Dirty(DIRTY_VIEWMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX) ;
}
if (++i >= end) {
break;
}
}
}
const int count = i;
gstate.viewmtxnum = (GE_CMD_VIEWMATRIXNUMBER << 24) | ((op & 0xF) + count);
// Skip over the loaded data, it's done now.
currentList->pc += count * 4;
UpdatePC(currentList->pc, currentList->pc);
}
void GPUCommonHW::Execute_ViewMtxData(u32 op, u32 diff) {
// Note: it's uncommon to get here now, see above.
int num = gstate.viewmtxnum & 0x00FFFFFF;
u32 newVal = op << 8;
if (num < 12 && newVal != ((const u32 *)gstate.viewMatrix)[num]) {
Flush();
((u32 *)gstate.viewMatrix)[num] = newVal;
gstate_c.Dirty(DIRTY_VIEWMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
num++;
gstate.viewmtxnum = (GE_CMD_VIEWMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.viewmtxdata = GE_CMD_VIEWMATRIXDATA << 24;
}
void GPUCommonHW::Execute_ProjMtxNum(u32 op, u32 diff) {
if (!currentList) {
gstate.projmtxnum = (GE_CMD_PROJMATRIXNUMBER << 24) | (op & 0xF);
return;
}
// This is almost always followed by GE_CMD_PROJMATRIXDATA.
const u32_le *src = (const u32_le *)Memory::GetPointerUnchecked(currentList->pc + 4);
u32 *dst = (u32 *)(gstate.projMatrix + (op & 0xF));
const int end = 16 - (op & 0xF);
int i = 0;
bool fastLoad = !debugRecording_;
if (currentList->pc < currentList->stall && currentList->pc + end * 4 >= currentList->stall) {
fastLoad = false;
}
if (fastLoad) {
while ((src[i] >> 24) == GE_CMD_PROJMATRIXDATA) {
const u32 newVal = src[i] << 8;
if (dst[i] != newVal) {
Flush();
dst[i] = newVal;
gstate_c.Dirty(DIRTY_PROJMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
if (++i >= end) {
break;
}
}
}
const int count = i;
gstate.projmtxnum = (GE_CMD_PROJMATRIXNUMBER << 24) | ((op & 0xF) + count);
// Skip over the loaded data, it's done now.
currentList->pc += count * 4;
UpdatePC(currentList->pc, currentList->pc);
}
void GPUCommonHW::Execute_ProjMtxData(u32 op, u32 diff) {
// Note: it's uncommon to get here now, see above.
int num = gstate.projmtxnum & 0x00FFFFFF;
u32 newVal = op << 8;
if (num < 16 && newVal != ((const u32 *)gstate.projMatrix)[num]) {
Flush();
((u32 *)gstate.projMatrix)[num] = newVal;
gstate_c.Dirty(DIRTY_PROJMATRIX | DIRTY_WORLD_VIEW_PROJ_MATRIX | DIRTY_VIEW_PROJ_MATRIX | DIRTY_CULL_MATRIX);
}
num++;
if (num <= 16)
gstate.projmtxnum = (GE_CMD_PROJMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.projmtxdata = GE_CMD_PROJMATRIXDATA << 24;
}
void GPUCommonHW::Execute_TgenMtxNum(u32 op, u32 diff) {
if (!currentList) {
gstate.texmtxnum = (GE_CMD_TGENMATRIXNUMBER << 24) | (op & 0xF);
return;
}
// This is almost always followed by GE_CMD_TGENMATRIXDATA.
const u32_le *src = (const u32_le *)Memory::GetPointerUnchecked(currentList->pc + 4);
u32 *dst = (u32 *)(gstate.tgenMatrix + (op & 0xF));
const int end = 12 - (op & 0xF);
int i = 0;
bool fastLoad = !debugRecording_ && end > 0;
if (currentList->pc < currentList->stall && currentList->pc + end * 4 >= currentList->stall) {
fastLoad = false;
}
if (fastLoad) {
while ((src[i] >> 24) == GE_CMD_TGENMATRIXDATA) {
const u32 newVal = src[i] << 8;
if (dst[i] != newVal) {
Flush();
dst[i] = newVal;
// We check the matrix to see if we need projection.
gstate_c.Dirty(DIRTY_TEXMATRIX | DIRTY_FRAGMENTSHADER_STATE);
}
if (++i >= end) {
break;
}
}
}
const int count = i;
gstate.texmtxnum = (GE_CMD_TGENMATRIXNUMBER << 24) | ((op & 0xF) + count);
// Skip over the loaded data, it's done now.
currentList->pc += count * 4;
UpdatePC(currentList->pc, currentList->pc);
}
void GPUCommonHW::Execute_TgenMtxData(u32 op, u32 diff) {
// Note: it's uncommon to get here now, see above.
int num = gstate.texmtxnum & 0x00FFFFFF;
u32 newVal = op << 8;
if (num < 12 && newVal != ((const u32 *)gstate.tgenMatrix)[num]) {
Flush();
((u32 *)gstate.tgenMatrix)[num] = newVal;
gstate_c.Dirty(DIRTY_TEXMATRIX | DIRTY_FRAGMENTSHADER_STATE); // We check the matrix to see if we need projection
}
num++;
gstate.texmtxnum = (GE_CMD_TGENMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.texmtxdata = GE_CMD_TGENMATRIXDATA << 24;
}
void GPUCommonHW::Execute_BoneMtxNum(u32 op, u32 diff) {
if (!currentList) {
gstate.boneMatrixNumber = (GE_CMD_BONEMATRIXNUMBER << 24) | (op & 0x7F);
return;
}
// This is almost always followed by GE_CMD_BONEMATRIXDATA.
const u32_le *src = (const u32_le *)Memory::GetPointerUnchecked(currentList->pc + 4);
u32 *dst = (u32 *)(gstate.boneMatrix + (op & 0x7F));
const int end = 12 * 8 - (op & 0x7F);
int i = 0;
bool fastLoad = !debugRecording_ && end > 0;
if (currentList->pc < currentList->stall && currentList->pc + end * 4 >= currentList->stall) {
fastLoad = false;
}
if (fastLoad) {
while ((src[i] >> 24) == GE_CMD_BONEMATRIXDATA) {
dst[i] = src[i] << 8;
if (++i >= end) {
break;
}
}
}
const int count = i;
gstate.boneMatrixNumber = (GE_CMD_BONEMATRIXNUMBER << 24) | ((op & 0x7F) + count);
// Skip over the loaded data, it's done now.
currentList->pc += count * 4;
UpdatePC(currentList->pc, currentList->pc);
}
void GPUCommonHW::Execute_BoneMtxData(u32 op, u32 diff) {
// Note: it's uncommon to get here now, see above.
int num = gstate.boneMatrixNumber & 0x00FFFFFF;
u32 newVal = op << 8;
if (num < 96 && newVal != ((const u32 *)gstate.boneMatrix)[num]) {
// Bone matrices should NOT flush, as we're always doing skinning in decode nowadays!
((u32 *)gstate.boneMatrix)[num] = newVal;
}
num++;
gstate.boneMatrixNumber = (GE_CMD_BONEMATRIXNUMBER << 24) | (num & 0x00FFFFFF);
gstate.boneMatrixData = GE_CMD_BONEMATRIXDATA << 24;
}
void GPUCommonHW::Execute_TexFlush(u32 op, u32 diff) {
// Games call this when they need the effect of drawing to be visible to texturing.
// And for a bunch of other reasons, but either way, this is what we need to do.
-11
View File
@@ -61,17 +61,6 @@ public:
void Execute_TexLevel(u32 op, u32 diff);
void Execute_LoadClut(u32 op, u32 diff);
void Execute_WorldMtxNum(u32 op, u32 diff);
void Execute_WorldMtxData(u32 op, u32 diff);
void Execute_ViewMtxNum(u32 op, u32 diff);
void Execute_ViewMtxData(u32 op, u32 diff);
void Execute_ProjMtxNum(u32 op, u32 diff);
void Execute_ProjMtxData(u32 op, u32 diff);
void Execute_TgenMtxNum(u32 op, u32 diff);
void Execute_TgenMtxData(u32 op, u32 diff);
void Execute_BoneMtxNum(u32 op, u32 diff);
void Execute_BoneMtxData(u32 op, u32 diff);
void Execute_TexFlush(u32 op, u32 diff);
// TODO: Have these return an error code if they jump to a bad address. If bad, stop the FastRunLoop.
+298
View File
@@ -0,0 +1,298 @@
// 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 <cstring>
#include "Common/Profiler/Profiler.h"
#include "Core/System.h"
#include "Core/Util/PPGeDraw.h"
#include "GPU/GPUCommon.h"
#include "GPU/GPUState.h"
#include "GPU/Common/DrawEngineCommon.h"
#include "GPU/Common/VertexDecoderCommon.h"
#include "GPU/Null/GPU_Null.h"
namespace {
// Only here for the vertex decoders (vertex sizes, bounding box tests). Queues and draws nothing.
class NullDrawEngine : public DrawEngineCommon {
public:
void DeviceLost() override {}
void DeviceRestore(Draw::DrawContext *draw) override {}
void Flush() override {}
void DispatchSubmitImm(GEPrimitiveType prim, TransformedVertex *buffer, int vertexCount, int cullMode, bool continuation) override {}
};
class GPU_Null : public GPUCommon {
public:
explicit GPU_Null(GraphicsContext *gfxCtx);
u32 CheckGPUFeatures() const override { return 0; }
void UpdateCmdInfo() override {}
void ExecuteOp(u32 op, u32 diff) override;
void GetStats(StringWriter &w) override {}
std::vector<const VirtualFramebuffer *> GetFramebufferList() const override { return {}; }
void DeviceLost() override { draw_ = nullptr; }
void DeviceRestore(Draw::DrawContext *draw) override {
draw_ = draw;
PPGeSetDrawContext(draw_);
}
void CheckDisplayResized() override { displayResized_ = false; }
void CheckConfigChanged(const DisplayLayoutConfig &config) override;
bool FramebufferDirty() override { return true; }
bool FramebufferReallyDirty() override { return true; }
void SetDisplayFramebuffer(u32 framebuf, u32 stride, GEBufferFormat format) override {
NotifyDisplay(framebuf, stride, format);
}
void SetCurFramebufferDirty(bool dirty) override {}
void PrepareCopyDisplayToOutput(const DisplayLayoutConfig &config) override {}
void CopyDisplayToOutput(const DisplayLayoutConfig &config) override {}
bool PresentedThisFrame() const override { return false; }
// Nothing is cached, and all of PSP memory is up to date, so the copies and sets are left to the caller.
void InvalidateCache(u32 addr, int size, GPUInvalidationType type) override {}
bool PerformMemoryCopy(u32 dest, u32 src, int size, GPUCopyFlag flags = GPUCopyFlag::NONE) override;
bool PerformMemorySet(u32 dest, u8 v, int size) override;
void PerformWriteFormattedFromMemory(u32 addr, int size, int width, GEBufferFormat format, int rectWidth) override {
// Remembered for EstimateFillCycles.
NoteVideoRange(addr, (u32)size);
}
bool PerformWriteStencilFromMemory(u32 dest, int size, WriteStencil flags) override { return false; }
bool PerformReadbackStencilToMemory(u32 dest, int size) override { return false; }
void Execute_Prim(u32 op, u32 diff);
void Execute_Bezier(u32 op, u32 diff);
void Execute_Spline(u32 op, u32 diff);
void Execute_BlockTransferStart(u32 op, u32 diff);
typedef void (GPU_Null::*CmdFunc)(u32 op, u32 diff);
protected:
void FastRunLoop(DisplayList &list) override;
void BuildReportingInfo() override {
reportingPrimaryInfo_ = "Null";
reportingFullInfo_ = "Null";
}
private:
void SkipCurve(int numPointsU, int numPointsV);
NullDrawEngine drawEngine_;
};
struct NullCommandInfo {
u8 flags;
GPU_Null::CmdFunc func;
};
static NullCommandInfo nullCmdInfo[256];
struct NullCommandTableEntry {
u8 cmd;
u8 flags;
GPU_Null::CmdFunc func;
};
// Only the commands that do something beyond setting their register. The rest only matter for drawing.
const NullCommandTableEntry nullCommandTable[] = {
{ GE_CMD_OFFSETADDR, FLAG_EXECUTE, &GPUCommon::Execute_OffsetAddr },
{ GE_CMD_ORIGIN, FLAG_EXECUTE | FLAG_READS_PC, &GPUCommon::Execute_Origin },
{ GE_CMD_JUMP, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, &GPUCommon::Execute_Jump },
{ GE_CMD_CALL, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, &GPUCommon::Execute_Call },
{ GE_CMD_RET, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, &GPUCommon::Execute_Ret },
{ GE_CMD_END, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, &GPUCommon::Execute_End },
{ GE_CMD_VADDR, FLAG_EXECUTE, &GPUCommon::Execute_Vaddr },
{ GE_CMD_IADDR, FLAG_EXECUTE, &GPUCommon::Execute_Iaddr },
{ GE_CMD_BJUMP, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, &GPUCommon::Execute_BJump },
{ GE_CMD_BOUNDINGBOX, FLAG_EXECUTE, &GPUCommon::Execute_BoundingBox },
{ GE_CMD_PRIM, FLAG_EXECUTE, &GPU_Null::Execute_Prim },
{ GE_CMD_BEZIER, FLAG_EXECUTE, &GPU_Null::Execute_Bezier },
{ GE_CMD_SPLINE, FLAG_EXECUTE, &GPU_Null::Execute_Spline },
{ GE_CMD_MORPHWEIGHT0, FLAG_EXECUTEONCHANGE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT1, FLAG_EXECUTEONCHANGE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT2, FLAG_EXECUTEONCHANGE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT3, FLAG_EXECUTEONCHANGE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT4, FLAG_EXECUTEONCHANGE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT5, FLAG_EXECUTEONCHANGE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT6, FLAG_EXECUTEONCHANGE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_MORPHWEIGHT7, FLAG_EXECUTEONCHANGE, &GPUCommon::Execute_MorphWeight },
{ GE_CMD_TRANSFERSTART, FLAG_EXECUTE | FLAG_READS_PC, &GPU_Null::Execute_BlockTransferStart },
{ GE_CMD_WORLDMATRIXNUMBER, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, &GPUCommon::Execute_WorldMtxNum },
{ GE_CMD_WORLDMATRIXDATA, FLAG_EXECUTE, &GPUCommon::Execute_WorldMtxData },
{ GE_CMD_VIEWMATRIXNUMBER, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, &GPUCommon::Execute_ViewMtxNum },
{ GE_CMD_VIEWMATRIXDATA, FLAG_EXECUTE, &GPUCommon::Execute_ViewMtxData },
{ GE_CMD_PROJMATRIXNUMBER, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, &GPUCommon::Execute_ProjMtxNum },
{ GE_CMD_PROJMATRIXDATA, FLAG_EXECUTE, &GPUCommon::Execute_ProjMtxData },
{ GE_CMD_TGENMATRIXNUMBER, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, &GPUCommon::Execute_TgenMtxNum },
{ GE_CMD_TGENMATRIXDATA, FLAG_EXECUTE, &GPUCommon::Execute_TgenMtxData },
{ GE_CMD_BONEMATRIXNUMBER, FLAG_EXECUTE | FLAG_READS_PC | FLAG_WRITES_PC, &GPUCommon::Execute_BoneMtxNum },
{ GE_CMD_BONEMATRIXDATA, FLAG_EXECUTE, &GPUCommon::Execute_BoneMtxData },
{ GE_CMD_VAP, FLAG_EXECUTE, &GPUCommon::Execute_ImmVertexAlphaPrim },
};
GPU_Null::GPU_Null(GraphicsContext *gfxCtx) : GPUCommon(gfxCtx, nullptr) {
memset(nullCmdInfo, 0, sizeof(nullCmdInfo));
for (const NullCommandTableEntry &entry : nullCommandTable) {
_dbg_assert_(nullCmdInfo[entry.cmd].func == nullptr);
nullCmdInfo[entry.cmd].flags = entry.flags;
nullCmdInfo[entry.cmd].func = entry.func;
}
drawEngine_.SetGPUCommon(this);
drawEngine_.Init();
drawEngineCommon_ = &drawEngine_;
// Nothing is queued, so there's nothing to flush.
flushOnParams_ = false;
BuildReportingInfo();
}
void GPU_Null::CheckConfigChanged(const DisplayLayoutConfig &config) {
if (configChanged_) {
drawEngine_.NotifyConfigChanged();
configChanged_ = false;
}
}
void GPU_Null::FastRunLoop(DisplayList &list) {
PROFILE_THIS_SCOPE("null_runloop");
if (!Memory::IsValidAddress(list.pc)) {
downcount = 0;
return;
}
const NullCommandInfo *cmdInfo = nullCmdInfo;
int dc = downcount;
for (; dc > 0; --dc) {
const u32 op = Memory::ReadUnchecked_U32(list.pc);
const u32 cmd = op >> 24;
const NullCommandInfo &info = cmdInfo[cmd];
const u32 diff = op ^ gstate.cmdmem[cmd];
if (diff) {
gstate.cmdmem[cmd] = op;
}
if ((info.flags & FLAG_EXECUTE) || (diff && (info.flags & FLAG_EXECUTEONCHANGE))) {
downcount = dc;
(this->*info.func)(op, diff);
dc = downcount;
}
list.pc += 4;
}
downcount = 0;
}
void GPU_Null::ExecuteOp(u32 op, u32 diff) {
const NullCommandInfo &info = nullCmdInfo[op >> 24];
if ((info.flags & FLAG_EXECUTE) || (diff && (info.flags & FLAG_EXECUTEONCHANGE))) {
(this->*info.func)(op, diff);
}
}
void GPU_Null::Execute_Prim(u32 op, u32 diff) {
const u32 count = op & 0xFFFF;
if (count == 0)
return;
FlushImm();
if (!Memory::IsValidAddress(gstate_c.vertexAddr)) {
ERROR_LOG(Log::G3D, "Bad vertex address %08x!", gstate_c.vertexAddr);
return;
}
const void *verts = Memory::GetPointerUnchecked(gstate_c.vertexAddr);
const void *inds = nullptr;
const u32 vertType = gstate.vertType;
if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) {
const int indexShift = ((vertType & GE_VTYPE_IDX_MASK) >> GE_VTYPE_IDX_SHIFT) - 1;
if (!Memory::IsValidRange(gstate_c.indexAddr, count << indexShift)) {
ERROR_LOG(Log::G3D, "Bad index address %08x (%d)!", gstate_c.indexAddr, count);
return;
}
inds = Memory::GetPointerUnchecked(gstate_c.indexAddr);
}
const VertexDecoder *dec = drawEngine_.GetVertexDecoder(GetVertTypeID(vertType, gstate.getUVGenMode()));
const GEPrimitiveType prim = static_cast<GEPrimitiveType>((op >> 16) & 7);
if (gstate.isModeThrough()) {
cyclesExecuted += EstimateFillCycles(prim, verts, inds, count, dec, vertType);
}
const int cycles = EstimatePerVertexCost() * count;
gpuStats.perFrame.vertexGPUCycles += cycles;
if (!PSP_CoreParameter().compat.flags().FastEmulatedGPU) {
cyclesExecuted += cycles;
}
gstate_c.AdvanceVerts(vertType, count, count * dec->VertexSize());
}
// Moves the pointers past a curve like DrawEngineCommon::SubmitCurve would.
void GPU_Null::SkipCurve(int numPointsU, int numPointsV) {
if (!Memory::IsValidAddress(gstate_c.vertexAddr)) {
ERROR_LOG(Log::G3D, "Bad vertex address %08x!", gstate_c.vertexAddr);
return;
}
const u32 vertType = gstate.vertType;
if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE && !Memory::IsValidAddress(gstate_c.indexAddr)) {
ERROR_LOG(Log::G3D, "Bad index address %08x!", gstate_c.indexAddr);
return;
}
const int count = numPointsU * numPointsV;
int bytesRead = 0;
// SubmitCurve draws nothing, and reads nothing, with fewer than four points either way.
if (numPointsU >= 4 && numPointsV >= 4) {
bytesRead = count * drawEngine_.GetVertexDecoder(GetVertTypeID(vertType, gstate.getUVGenMode()))->VertexSize();
}
gstate_c.AdvanceVerts(vertType, count, bytesRead);
}
void GPU_Null::Execute_Bezier(u32 op, u32 diff) {
SkipCurve(op & 0xFF, (op >> 8) & 0xFF);
}
void GPU_Null::Execute_Spline(u32 op, u32 diff) {
SkipCurve(op & 0xFF, (op >> 8) & 0xFF);
}
void GPU_Null::Execute_BlockTransferStart(u32 op, u32 diff) {
DoBlockTransfer(gstate_c.skipDrawReason);
}
bool GPU_Null::PerformMemoryCopy(u32 dest, u32 src, int size, GPUCopyFlag flags) {
if (!(flags & GPUCopyFlag::DEBUG_NOTIFIED))
recorder_.NotifyMemcpy(dest, src, size);
return false;
}
bool GPU_Null::PerformMemorySet(u32 dest, u8 v, int size) {
recorder_.NotifyMemset(dest, v, size);
return false;
}
} // namespace
GPUCommon *CreateNullGPU(GraphicsContext *gfxCtx) {
return new GPU_Null(gfxCtx);
}
+26
View File
@@ -0,0 +1,26 @@
// 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/.
#pragma once
class GPUCommon;
class GraphicsContext;
// A GPU that runs display lists (flow control, bounding boxes, matrices, block transfers, the vertex and
// index pointers moving past each draw, and the GE timing) but draws nothing, and needs no thin3d context.
// For measuring the CPU emulation without graphics work in the way (headless --graphics=null).
GPUCommon *CreateNullGPU(GraphicsContext *gfxCtx);
+2
View File
@@ -148,6 +148,7 @@
<ClInclude Include="..\..\GPU\Software\RasterizerTypes.h" />
<ClInclude Include="..\..\GPU\Software\Sampler.h" />
<ClInclude Include="..\..\GPU\Software\SoftGpu.h" />
<ClInclude Include="..\..\GPU\Null\GPU_Null.h" />
<ClInclude Include="..\..\GPU\Software\TransformUnit.h" />
<ClInclude Include="..\..\GPU\Software\GEMath.h" />
<ClInclude Include="pch.h" />
@@ -216,6 +217,7 @@
<ClCompile Include="..\..\GPU\Software\RasterizerRectangle.cpp" />
<ClCompile Include="..\..\GPU\Software\Sampler.cpp" />
<ClCompile Include="..\..\GPU\Software\SoftGpu.cpp" />
<ClCompile Include="..\..\GPU\Null\GPU_Null.cpp" />
<ClCompile Include="..\..\GPU\Software\TransformUnit.cpp" />
<ClCompile Include="..\..\GPU\Software\GEMath.cpp" />
<ClCompile Include="pch.cpp">
+2
View File
@@ -45,6 +45,7 @@
<ClCompile Include="..\..\GPU\Software\Rasterizer.cpp" />
<ClCompile Include="..\..\GPU\Software\Sampler.cpp" />
<ClCompile Include="..\..\GPU\Software\SoftGpu.cpp" />
<ClCompile Include="..\..\GPU\Null\GPU_Null.cpp" />
<ClCompile Include="..\..\GPU\Software\TransformUnit.cpp" />
<ClCompile Include="..\..\GPU\Software\GEMath.cpp" />
<ClCompile Include="pch.cpp" />
@@ -125,6 +126,7 @@
<ClInclude Include="..\..\GPU\Software\Rasterizer.h" />
<ClInclude Include="..\..\GPU\Software\Sampler.h" />
<ClInclude Include="..\..\GPU\Software\SoftGpu.h" />
<ClInclude Include="..\..\GPU\Null\GPU_Null.h" />
<ClInclude Include="..\..\GPU\Software\TransformUnit.h" />
<ClInclude Include="..\..\GPU\Software\GEMath.h" />
<ClInclude Include="pch.h" />
+1
View File
@@ -640,6 +640,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/GPU/Software/RasterizerRectangle.cpp.arm \
$(SRC)/GPU/Software/Sampler.cpp \
$(SRC)/GPU/Software/SoftGpu.cpp \
$(SRC)/GPU/Null/GPU_Null.cpp \
$(SRC)/GPU/Software/TransformUnit.cpp \
$(SRC)/Core/ELF/ElfReader.cpp \
$(SRC)/Core/ELF/PBPReader.cpp \
+6
View File
@@ -251,6 +251,12 @@ anything else that uses the CPU, including a forgotten `usbhostfs_pc`, which can
that exits with an error, misses `TIMEOUT (emulated)` or doesn't load its savestate stops the tool rather than
passing as a fast run.
To measure the CPU emulation alone, use `--graphics=null` (headless only, `GPU/Null/GPU_Null.cpp`). It runs
display lists, bounding boxes, matrix loads and block transfers, moves the vertex and index pointers past
each draw, and charges the same GE time per draw, but draws nothing and creates no draw context. Games flip
the same frames as with Vulkan, in roughly half the host instructions. Nothing is rendered, so screenshots
and anything that reads a framebuffer back through the GPU (rather than from PSP memory) get nothing.
Headless decodes vertices with the JIT like the app, where the host can (`--vertex-jit=false` for the C++
decoder, which the tests don't need either).
+5 -2
View File
@@ -1061,15 +1061,18 @@ int main(int argc, const char* argv[]) {
break;
}
}
if (cmdLineOptions.nullGraphics.value_or(false)) {
gpuCore = GPUCORE_NULL;
}
// Time to set up graphics
GraphicsContext *graphicsContext = nullptr;
std::string *deviceSetting = nullptr;
void *window = nullptr;
WindowDesc windowDesc;
if (g_Config.bSoftwareRendering) {
if (g_Config.bSoftwareRendering || gpuCore == GPUCORE_NULL) {
// For software rendering, we just create a dummy graphics context (to share as much code as possible).
// We don't bother with a window.
// We don't bother with a window. The null GPU needs no draw context either.
graphicsContext = new NullGraphicsContext();
} else {
#if defined(HEADLESS_NO_SDL)
+1
View File
@@ -587,6 +587,7 @@ SOURCES_CXX += \
$(GPUDIR)/Software/TransformUnit.cpp \
$(GPUDIR)/Software/GEMath.cpp \
$(GPUDIR)/Software/SoftGpu.cpp \
$(GPUDIR)/Null/GPU_Null.cpp \
$(GPUDIR)/Software/Sampler.cpp \
$(GPUDIR)/GeConstants.cpp \
$(GPUDIR)/GeDisasm.cpp \