From c440d53160673b9065264f20a77246f9ff894956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 16 Oct 2026 01:12:26 +0200 Subject: [PATCH 1/3] GPU: Move the hardware backends' matrix commands up to GPUCommon So a backend that isn't GPUCommonHW can use them. No change in behaviour. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/GPUCommon.cpp | 263 ++++++++++++++++++++++++++++++++++++++++++++ GPU/GPUCommon.h | 12 ++ GPU/GPUCommonHW.cpp | 263 -------------------------------------------- GPU/GPUCommonHW.h | 11 -- 4 files changed, 275 insertions(+), 274 deletions(-) diff --git a/GPU/GPUCommon.cpp b/GPU/GPUCommon.cpp index 6c17295cc4..113ebad623 100644 --- a/GPU/GPUCommon.cpp +++ b/GPU/GPUCommon.cpp @@ -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; diff --git a/GPU/GPUCommon.h b/GPU/GPUCommon.h index 04a57ba4e2..12ab3eb933 100644 --- a/GPU/GPUCommon.h +++ b/GPU/GPUCommon.h @@ -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(); diff --git a/GPU/GPUCommonHW.cpp b/GPU/GPUCommonHW.cpp index 269435a77a..13daa1cc7e 100644 --- a/GPU/GPUCommonHW.cpp +++ b/GPU/GPUCommonHW.cpp @@ -1471,269 +1471,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. diff --git a/GPU/GPUCommonHW.h b/GPU/GPUCommonHW.h index 69640402f5..a006173365 100644 --- a/GPU/GPUCommonHW.h +++ b/GPU/GPUCommonHW.h @@ -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. From 1d7487e5a7f5d9c52c59d5549d5affda8b6c7b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 16 Oct 2026 01:12:38 +0200 Subject: [PATCH 2/3] GPU: Add a null GPU, and headless --graphics=null to use it GPU_Null derives straight from GPUCommon and has no thin3d context, framebuffer manager, texture cache or shaders. It runs display lists with the flow control, bounding boxes, matrix loads (the hardware backends'), immediate vertices and block transfers of the others, moves the vertex and index pointers past each draw and curve, and charges the per-vertex and fill cost the software renderer does. A DrawEngineCommon that queues nothing provides the vertex decoders. It's for measuring the CPU emulation. From savestates, God of War, Tekken 6, Wipeout Pure and LocoRoco flip the same frames in 10 emulated seconds as with Vulkan and software, in 44-77% fewer instructions than Vulkan. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/CmdLine.cpp | 7 + Core/CmdLine.h | 2 + Core/CoreParameter.h | 1 + Core/System.cpp | 2 +- GPU/CMakeLists.txt | 2 + GPU/GPU.cpp | 5 +- GPU/GPU.vcxproj | 2 + GPU/GPU.vcxproj.filters | 9 + GPU/Null/GPU_Null.cpp | 298 ++++++++++++++++++++++++++++ GPU/Null/GPU_Null.h | 26 +++ UWP/GPU_UWP/GPU_UWP.vcxproj | 2 + UWP/GPU_UWP/GPU_UWP.vcxproj.filters | 2 + android/jni/Android.mk | 1 + docs/debugging.md | 6 + headless/Headless.cpp | 7 +- libretro/Makefile.common | 1 + 16 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 GPU/Null/GPU_Null.cpp create mode 100644 GPU/Null/GPU_Null.h diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index b3119cd4c2..79d0a04069 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -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; diff --git a/Core/CmdLine.h b/Core/CmdLine.h index da797d26c0..720ce698db 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -33,6 +33,8 @@ struct CommandLineOptions { std::optional fullscreen; std::optional gpuBackend; std::optional softwareRendering; + // --graphics=null: headless only, see GPU_Null.h. + std::optional nullGraphics; std::optional enableLogging; std::optional logLevel; // Override log level with this. std::optional log; diff --git a/Core/CoreParameter.h b/Core/CoreParameter.h index 9db8701b8c..195a49d25a 100644 --- a/Core/CoreParameter.h +++ b/Core/CoreParameter.h @@ -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 { diff --git a/Core/System.cpp b/Core/System.cpp index 7c9567ade7..d5254c5c99 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -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; } diff --git a/GPU/CMakeLists.txt b/GPU/CMakeLists.txt index 2bb8284fcc..a17a4a48a5 100644 --- a/GPU/CMakeLists.txt +++ b/GPU/CMakeLists.txt @@ -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 ) diff --git a/GPU/GPU.cpp b/GPU/GPU.cpp index d9502fc6e8..b6161d6ab3 100644 --- a/GPU/GPU.cpp +++ b/GPU/GPU.cpp @@ -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); diff --git a/GPU/GPU.vcxproj b/GPU/GPU.vcxproj index 37b12e9950..760262b184 100644 --- a/GPU/GPU.vcxproj +++ b/GPU/GPU.vcxproj @@ -347,6 +347,7 @@ + @@ -471,6 +472,7 @@ + diff --git a/GPU/GPU.vcxproj.filters b/GPU/GPU.vcxproj.filters index 300e791cdc..50c96af50e 100644 --- a/GPU/GPU.vcxproj.filters +++ b/GPU/GPU.vcxproj.filters @@ -4,6 +4,9 @@ {f7563dba-8146-4c21-a092-e864ff145d79} + + {9879c213-d775-44fc-b1e4-a9d19ac2dff5} + {4f6d1284-2c23-4ebc-842c-666a1305bfed} @@ -48,6 +51,9 @@ Software + + Null + Software @@ -299,6 +305,9 @@ Software + + Null + Software diff --git a/GPU/Null/GPU_Null.cpp b/GPU/Null/GPU_Null.cpp new file mode 100644 index 0000000000..2fa25caf7a --- /dev/null +++ b/GPU/Null/GPU_Null.cpp @@ -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 + +#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 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((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); +} diff --git a/GPU/Null/GPU_Null.h b/GPU/Null/GPU_Null.h new file mode 100644 index 0000000000..a812413317 --- /dev/null +++ b/GPU/Null/GPU_Null.h @@ -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); diff --git a/UWP/GPU_UWP/GPU_UWP.vcxproj b/UWP/GPU_UWP/GPU_UWP.vcxproj index 2f24fbac61..229411b40b 100644 --- a/UWP/GPU_UWP/GPU_UWP.vcxproj +++ b/UWP/GPU_UWP/GPU_UWP.vcxproj @@ -148,6 +148,7 @@ + @@ -216,6 +217,7 @@ + diff --git a/UWP/GPU_UWP/GPU_UWP.vcxproj.filters b/UWP/GPU_UWP/GPU_UWP.vcxproj.filters index 87fb58b2f8..44d72b8ee2 100644 --- a/UWP/GPU_UWP/GPU_UWP.vcxproj.filters +++ b/UWP/GPU_UWP/GPU_UWP.vcxproj.filters @@ -45,6 +45,7 @@ + @@ -125,6 +126,7 @@ + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 524a2b5979..39b2f05ee3 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -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 \ diff --git a/docs/debugging.md b/docs/debugging.md index ef3a967d45..67af0b05fe 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -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). diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 089545a6a3..292d6906c5 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -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) diff --git a/libretro/Makefile.common b/libretro/Makefile.common index 21bbb7b3c0..7dab17fab4 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -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 \ From b3c92cbb3b903b7500394fe8116ca5aa19cdfe77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 16 Oct 2026 01:21:04 +0200 Subject: [PATCH 3/3] GPU: Skip discarded AA lines like culled draws The hardware backends discard antialiased lines drawn with REPLACE (the heuristic from #6483 and #12588) by returning from Execute_Prim before the vertex or index pointer moves past them, and without taking their time. A game that draws on from where they end then reads the wrong vertices. Skip them like a culled draw instead, which does both, as the software renderer and the null GPU do. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/GPUCommonHW.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/GPU/GPUCommonHW.cpp b/GPU/GPUCommonHW.cpp index 13daa1cc7e..26e00da8ec 100644 --- a/GPU/GPUCommonHW.cpp +++ b/GPU/GPUCommonHW.cpp @@ -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((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