Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/PS1/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ ${CMAKE_CURRENT_SOURCE_DIR}/PS1TIM.h
if(ENABLE_PS1_RIP)
list(APPEND SRC_FILES
${CMAKE_CURRENT_SOURCE_DIR}/runtime/CaptureBuffer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/runtime/CaptureSnapshot.cpp
${CMAKE_CURRENT_SOURCE_DIR}/runtime/EmuCoreLoader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/runtime/GteInverse.cpp
${CMAKE_CURRENT_SOURCE_DIR}/runtime/EmuViewport.cpp
${CMAKE_CURRENT_SOURCE_DIR}/runtime/GpuCommandParser.cpp
${CMAKE_CURRENT_SOURCE_DIR}/runtime/GteCapture.cpp
${CMAKE_CURRENT_SOURCE_DIR}/runtime/MeshReconstructor.cpp
${CMAKE_CURRENT_SOURCE_DIR}/runtime/PS1RipMeshBuilder.cpp
${CMAKE_CURRENT_SOURCE_DIR}/runtime/PS1RipLegalityDialog.cpp
${CMAKE_CURRENT_SOURCE_DIR}/runtime/PS1RipManager.cpp
${CMAKE_CURRENT_SOURCE_DIR}/runtime/PS1RipSessionWindow.cpp
Expand All @@ -38,7 +42,11 @@ if(ENABLE_PS1_RIP)
)
list(APPEND HEADER_FILES
${CMAKE_CURRENT_SOURCE_DIR}/runtime/CaptureBuffer.h
${CMAKE_CURRENT_SOURCE_DIR}/runtime/CaptureSnapshot.h
${CMAKE_CURRENT_SOURCE_DIR}/runtime/CaptureTypes.h
${CMAKE_CURRENT_SOURCE_DIR}/runtime/GteInverse.h
${CMAKE_CURRENT_SOURCE_DIR}/runtime/MeshReconstructor.h
${CMAKE_CURRENT_SOURCE_DIR}/runtime/PS1RipMeshBuilder.h
${CMAKE_CURRENT_SOURCE_DIR}/runtime/EmuCore.h
${CMAKE_CURRENT_SOURCE_DIR}/runtime/EmuCoreLoader.h
${CMAKE_CURRENT_SOURCE_DIR}/runtime/EmuFramebuffer.h
Expand Down
9 changes: 9 additions & 0 deletions src/PS1/PS1_RIP_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ See epic #412 for phased issues (#413–#431).
- `dumpVRAM()` saves `<AppData>/ps1_rip/captures/<id>_vram.png` and feeds `VramViewerWidget` in the session window.
- Stub core fills CLUT + 4/8/15 bpp test regions each frame via `stubFillVramPattern`.

## Phase 4 status (#422)

- `CaptureSnapshot` copies worker `CaptureBuffer` to the main thread for reconstruction.
- `GteInverse` approximates GTE screen→model un-projection; PS1 Y-down → editor Y-up.
- `MeshReconstructor` groups primitives by `matrixId` + texture key, triangulates quads, emits vertex color + UV.
- `PS1RipMeshBuilder` creates Ogre mesh/submeshes and attaches `PS1Capture_<id>` to the live scene via `Manager`.
- `captureFrame` builds mesh automatically; session toolbar adds **Arm Capture** / **Capture Frame**.
- Sentry breadcrumb `ps1.rip.mesh.built` with vertex/triangle counts.

## Open questions

- mednafen-psx plugin build/integration (replace stub for real emulation).
Expand Down
11 changes: 11 additions & 0 deletions src/PS1/runtime/CaptureSnapshot.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#include "CaptureSnapshot.h"
#include "CaptureBuffer.h"

CaptureSnapshot CaptureSnapshot::fromBuffer(const CaptureBuffer &buffer)
{
CaptureSnapshot snap;
snap.prims = buffer.prims();
snap.matrices = buffer.matrices();
snap.cameraMatrixId = buffer.cameraMatrixId();
return snap;
}
23 changes: 23 additions & 0 deletions src/PS1/runtime/CaptureSnapshot.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#ifndef CAPTURESNAPSHOT_H
#define CAPTURESNAPSHOT_H

#include "CaptureTypes.h"

#include <QMetaType>
#include <QVector>

class CaptureBuffer;

/** Thread-safe copy of a frame capture for main-thread reconstruction (#422). */
struct CaptureSnapshot
{
QVector<PrimRecord> prims;
QVector<MatrixRecord> matrices;
uint32_t cameraMatrixId = UINT32_MAX;

static CaptureSnapshot fromBuffer(const CaptureBuffer &buffer);
};

Q_DECLARE_METATYPE(CaptureSnapshot)

#endif // CAPTURESNAPSHOT_H
49 changes: 49 additions & 0 deletions src/PS1/runtime/GteInverse.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include "GteInverse.h"

#include <cmath>

namespace GteInverse {

bool screenToModel(const MatrixRecord &matrix, int sx, int sy, int sz, float &mx, float &my, float &mz)
{
const double h = matrix.h != 0 ? static_cast<double>(matrix.h) : 4096.0;
const double ir3 = sz != 0 ? static_cast<double>(sz) : 4096.0;
const double ir1 = ((static_cast<double>(sx) * 65536.0) - matrix.ofx) * ir3 / h;
const double ir2 = ((static_cast<double>(sy) * 65536.0) - matrix.ofy) * ir3 / h;

const double irAdj[3] = {ir1 - matrix.tr[0], ir2 - matrix.tr[1], ir3 - matrix.tr[2]};

Check warning on line 14 in src/PS1/runtime/GteInverse.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "std::array" or "std::vector" instead of a C-style array.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ45qIEZC2v0iRA_3Ah8&open=AZ45qIEZC2v0iRA_3Ah8&pullRequest=601
double vx = 0.0;
double vy = 0.0;
double vz = 0.0;
for (int c = 0; c < 3; ++c) {
vx += static_cast<double>(matrix.rt.m[c][0]) * irAdj[c];
vy += static_cast<double>(matrix.rt.m[c][1]) * irAdj[c];
vz += static_cast<double>(matrix.rt.m[c][2]) * irAdj[c];
}

constexpr double kFixedScale = 1.0 / 4096.0;
mx = static_cast<float>(vx * kFixedScale);
my = static_cast<float>(vy * kFixedScale);
mz = static_cast<float>(vz * kFixedScale);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return std::isfinite(mx) && std::isfinite(my) && std::isfinite(mz);
}

void psxScreenToWorld(float sx, float sy, float sz, float &wx, float &wy, float &wz)
{
constexpr float kCenterX = 160.0f;
constexpr float kCenterY = 120.0f;
constexpr float kScale = 0.01f;
wx = (sx - kCenterX) * kScale;
wy = -(sy - kCenterY) * kScale;
wz = sz * kScale;
}

void modelToEditor(float mx, float my, float mz, float &wx, float &wy, float &wz)
{
constexpr float kScale = 0.01f;
wx = mx * kScale;
wy = -my * kScale;
wz = -mz * kScale;
}

} // namespace GteInverse
19 changes: 19 additions & 0 deletions src/PS1/runtime/GteInverse.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef GTEINVERSE_H
#define GTEINVERSE_H

#include "CaptureTypes.h"

/** Approximate inverse of PS1 GTE screen projection (#422). */
namespace GteInverse {

bool screenToModel(const MatrixRecord &matrix, int sx, int sy, int sz, float &mx, float &my, float &mz);

/** PS1 screen (Y-down) → editor world (Y-up, right-handed). */
void psxScreenToWorld(float sx, float sy, float sz, float &wx, float &wy, float &wz);

/** GTE model space → editor world (handedness flip only, no screen re-centering). */
void modelToEditor(float mx, float my, float mz, float &wx, float &wy, float &wz);

} // namespace GteInverse

#endif // GTEINVERSE_H
43 changes: 43 additions & 0 deletions src/PS1/runtime/GteInverse_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#include "GteInverse.h"

#include <cmath>
#include <gtest/gtest.h>

static MatrixRecord identityMatrix()
{
MatrixRecord m{};
m.rt.m[0][0] = 1 << 12;
m.rt.m[1][1] = 1 << 12;
m.rt.m[2][2] = 1 << 12;
m.h = 256;
return m;
}

TEST(GteInverseTest, ScreenToModelWithTranslation)
{
MatrixRecord matrix = identityMatrix();
matrix.tr[0] = 4096;
matrix.tr[1] = 0;
matrix.tr[2] = 0;

float mx = 0.0f;
float my = 0.0f;
float mz = 0.0f;
ASSERT_TRUE(GteInverse::screenToModel(matrix, 160, 120, 4096, mx, my, mz));
EXPECT_TRUE(std::isfinite(mx));
EXPECT_TRUE(std::isfinite(my));
EXPECT_TRUE(std::isfinite(mz));
EXPECT_NE(mx, 0.0f);
}

TEST(GteInverseTest, ScreenToWorldFlipsY)
{
float wx = 0.0f;
float wy = 0.0f;
float wz = 0.0f;
GteInverse::psxScreenToWorld(160.0f, 120.0f, 0.0f, wx, wy, wz);
EXPECT_NEAR(wx, 0.0f, 1e-4f);
EXPECT_NEAR(wy, 0.0f, 1e-4f);
GteInverse::psxScreenToWorld(160.0f, 140.0f, 0.0f, wx, wy, wz);
EXPECT_LT(wy, 0.0f);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
144 changes: 144 additions & 0 deletions src/PS1/runtime/MeshReconstructor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#include "MeshReconstructor.h"

#include "GteInverse.h"

#include <OgreColourValue.h>

#include <QHash>

namespace {

uint32_t packDiffuse(uint8_t r, uint8_t g, uint8_t b)
{
const Ogre::ColourValue cv(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f);
return cv.getAsBYTE();
}

QString textureMaterialName(uint16_t tpage, uint16_t clut)
{
return QStringLiteral("PS1Rip/tpage_%1_clut_%2")
.arg(tpage, 4, 16, QChar('0'))
.arg(clut, 4, 16, QChar('0'));
}

quint64 textureGroupKey(uint16_t tpage, uint16_t clut)
{
return (static_cast<quint64>(tpage) << 32) | clut;
}

struct SubMeshAccumulator {
QString materialName;
QVector<ReconstructedVertex> vertices;
QVector<uint32_t> indices;

void addTriangle(const ReconstructedVertex &a, const ReconstructedVertex &b, const ReconstructedVertex &c)
{
const uint32_t base = static_cast<uint32_t>(vertices.size());

Check warning on line 36 in src/PS1/runtime/MeshReconstructor.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace the redundant type with "auto".

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ45qH9OC2v0iRA_3Ah5&open=AZ45qH9OC2v0iRA_3Ah5&pullRequest=601
vertices.append(a);
vertices.append(b);
vertices.append(c);
indices.append(base);
indices.append(base + 1);
indices.append(base + 2);
}
};

ReconstructedVertex vertexFromPsx(const PsxVertex &v, const MatrixRecord *matrix, bool textured)
{
ReconstructedVertex out;
out.diffuseArgb = packDiffuse(v.r, v.g, v.b);

float mx = 0.0f;
float my = 0.0f;
float mz = 0.0f;
if (matrix && GteInverse::screenToModel(*matrix, v.x, v.y, v.z, mx, my, mz))

Check warning on line 54 in src/PS1/runtime/MeshReconstructor.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "mz" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ45qH9PC2v0iRA_3Ah6&open=AZ45qH9PC2v0iRA_3Ah6&pullRequest=601
GteInverse::modelToEditor(mx, my, mz, out.px, out.py, out.pz);
else
GteInverse::psxScreenToWorld(static_cast<float>(v.x), static_cast<float>(v.y),
static_cast<float>(v.z), out.px, out.py, out.pz);

if (textured) {
out.u = static_cast<float>(v.u) / 256.0f;
out.v = static_cast<float>(v.v) / 256.0f;
}
return out;
}

void emitPrimitive(const PrimRecord &prim, const MatrixRecord *matrix, SubMeshAccumulator &acc)
{
const bool textured = prim.kind == PrimKind::TexturedTri || prim.kind == PrimKind::TexturedQuad
|| prim.kind == PrimKind::Sprite;

auto vtx = [&](int i) { return vertexFromPsx(prim.verts[i], matrix, textured); };

if (prim.kind == PrimKind::MonoTri || prim.kind == PrimKind::ShadedTri
|| prim.kind == PrimKind::TexturedTri) {
if (prim.vertexCount >= 3)
acc.addTriangle(vtx(0), vtx(1), vtx(2));
return;
}

if (prim.kind == PrimKind::MonoQuad || prim.kind == PrimKind::ShadedQuad
|| prim.kind == PrimKind::TexturedQuad) {
if (prim.vertexCount >= 4) {
acc.addTriangle(vtx(0), vtx(1), vtx(2));
acc.addTriangle(vtx(0), vtx(2), vtx(3));
}
return;
}

if (prim.kind == PrimKind::Sprite && prim.vertexCount >= 2) {
ReconstructedVertex a = vtx(0);
ReconstructedVertex b = vtx(1);
ReconstructedVertex c = b;
ReconstructedVertex d = a;
c.py = b.py + 0.05f;
d.py = a.py + 0.05f;
acc.addTriangle(a, b, c);
acc.addTriangle(a, c, d);
}
}

} // namespace

ReconstructedMesh MeshReconstructor::reconstruct(const CaptureSnapshot &snapshot)
{
ReconstructedMesh result;
if (snapshot.prims.isEmpty())
return result;

QHash<uint32_t, QHash<quint64, SubMeshAccumulator>> groupsByMatrix;

for (const PrimRecord &prim : snapshot.prims) {
const MatrixRecord *matrix = nullptr;
if (prim.matrixId < static_cast<uint32_t>(snapshot.matrices.size()))
matrix = &snapshot.matrices[static_cast<int>(prim.matrixId)];

const quint64 texKey = textureGroupKey(prim.tpage, prim.clut);
SubMeshAccumulator &acc = groupsByMatrix[prim.matrixId][texKey];
if (acc.materialName.isEmpty())
acc.materialName = textureMaterialName(prim.tpage, prim.clut);
emitPrimitive(prim, matrix, acc);
}

int subIndex = 0;
for (auto matIt = groupsByMatrix.constBegin(); matIt != groupsByMatrix.constEnd(); ++matIt) {
for (auto texIt = matIt.value().constBegin(); texIt != matIt.value().constEnd(); ++texIt) {
const SubMeshAccumulator &acc = texIt.value();
if (acc.vertices.isEmpty() || acc.indices.size() < 3)
continue;

ReconstructedSubMesh sub;
sub.materialName = acc.materialName;
sub.vertices = acc.vertices;
sub.indices = acc.indices;
result.subMeshes.append(sub);
result.vertexCount += acc.vertices.size();
result.triangleCount += acc.indices.size() / 3;
++subIndex;
}
}

result.meshName = QStringLiteral("ps1_capture");
return result;
}
44 changes: 44 additions & 0 deletions src/PS1/runtime/MeshReconstructor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#ifndef MESHRECONSTRUCTOR_H
#define MESHRECONSTRUCTOR_H

#include "CaptureSnapshot.h"

#include <QString>
#include <QVector>

/** One vertex in reconstructed editor space (#422). */
struct ReconstructedVertex {
float px = 0.0f;
float py = 0.0f;
float pz = 0.0f;
float nx = 0.0f;
float ny = 0.0f;
float nz = 0.0f;
Comment thread
fernandotonon marked this conversation as resolved.
float u = 0.0f;
float v = 0.0f;
uint32_t diffuseArgb = 0xFFFFFFFFu;
};

struct ReconstructedSubMesh {
QString materialName;
QVector<ReconstructedVertex> vertices;
QVector<uint32_t> indices;
};

struct ReconstructedMesh {
QString meshName;
QVector<ReconstructedSubMesh> subMeshes;
int vertexCount = 0;
int triangleCount = 0;

bool isEmpty() const { return subMeshes.isEmpty(); }
};

/** Builds editor-space meshes from a captured primitive stream (#422). */
class MeshReconstructor
{
public:
static ReconstructedMesh reconstruct(const CaptureSnapshot &snapshot);
};

#endif // MESHRECONSTRUCTOR_H
Loading
Loading