diff --git a/framework/files/runtime/include/gpu_temporal_match.h b/framework/files/runtime/include/gpu_temporal_match.h index 2b4e44f..270363d 100644 --- a/framework/files/runtime/include/gpu_temporal_match.h +++ b/framework/files/runtime/include/gpu_temporal_match.h @@ -272,7 +272,7 @@ static inline void gpu_temporal_lerp(const GpuTemporalTriangle *prev, ((double)curr->x[vertex] - prev->x[vertex]) * alpha) * scale); result.y[vertex] = (float)(((double)prev->y[vertex] + ((double)curr->y[vertex] - prev->y[vertex]) * alpha) * scale); - result.w[vertex] = (float)weight; + result.w[vertex] = (float)weight != 0.0f ? (float)weight : -1e-30f; /* 0 would read as a corner without a weight */ result.q[vertex] = (float)((double)prev->q[vertex] + ((double)curr->q[vertex] - prev->q[vertex]) * alpha); continue; @@ -287,4 +287,12 @@ static inline void gpu_temporal_lerp(const GpuTemporalTriangle *prev, *out = result; } +/* The depth of a corner gpu_temporal_lerp placed: a weight is the corner's depth there over its depth at an unweighted end. */ +static inline float gpu_temporal_lerp_depth(const GpuTemporalTriangle *curr, + const GpuTemporalTriangle *lerped, int vertex) { + const float after = curr->w[vertex] != 0.0f ? curr->w[vertex] : 1.0f; + const float here = lerped->w[vertex] != 0.0f ? lerped->w[vertex] : 1.0f; + return here > 0.0f ? curr->depth[vertex] * here / after : curr->depth[vertex]; +} + #endif /* PSXRECOMP_GPU_TEMPORAL_MATCH_H */ diff --git a/framework/files/runtime/src/gpu_gl_temporal.c.inc b/framework/files/runtime/src/gpu_gl_temporal.c.inc index 4585a1a..ec95863 100644 --- a/framework/files/runtime/src/gpu_gl_temporal.c.inc +++ b/framework/files/runtime/src/gpu_gl_temporal.c.inc @@ -837,7 +837,7 @@ static void temporal_underlay(float alpha,int covered) { for (int k=0;k<3;++k) { float *v=&s_temp_under[(j+k)*TEXV]; v[0]=v[t?20:7]=show?out.x[k]:0; v[1]=v[t?21:8]=show?out.y[k]:0; - v[t?23:9]=show?temporal_order(p->gone_to[ti].depth[k]*(covered?TEMP_ORDER_CLEARANCE:1.0f)):TEMP_ORDER_FAR; + v[t?23:9]=show?temporal_order(gpu_temporal_lerp_depth(&p->gone_to[ti],&out,k)*(covered?TEMP_ORDER_CLEARANCE:1.0f)):TEMP_ORDER_FAR; } any|=show; } @@ -890,6 +890,7 @@ static int temporal_present(float alpha,int lx,int ly,int lw,int lh) { float *v=&s_temp_scratch[(j+k)*TEXV]; v[0]=v[t?20:7]=out.x[facing?k:0]; v[1]=v[t?21:8]=out.y[facing?k:0]; if (t) v[22]=out.q[k]; + if (now->depth[k]>0.0f) v[t?23:9]=temporal_order(gpu_temporal_lerp_depth(now,&out,k)); } } else for (int j=0;jcount;++j) s_temp_scratch[j*TEXV+9]=c->first_tri<=first_world || (s_world_depth && c->first_tri<=last_world) diff --git a/tests/contracts/test_temporal_presenter_contract.py b/tests/contracts/test_temporal_presenter_contract.py index 27ec92e..21eb9ab 100644 --- a/tests/contracts/test_temporal_presenter_contract.py +++ b/tests/contracts/test_temporal_presenter_contract.py @@ -82,7 +82,8 @@ def shader(source: str, name: str) -> str: ) require( "glDepthFunc(covered ? GL_LESS : GL_ALWAYS); glDepthMask(covered ? GL_FALSE : GL_TRUE);" in underlay - and "v[t?23:9]=show?temporal_order(p->gone_to[ti].depth[k]*(covered?TEMP_ORDER_CLEARANCE:1.0f)):TEMP_ORDER_FAR;" in underlay + and "v[t?23:9]=show?temporal_order(gpu_temporal_lerp_depth(&p->gone_to[ti],&out,k)*(covered?TEMP_ORDER_CLEARANCE:1.0f)):TEMP_ORDER_FAR;" + in underlay and "#define TEMP_ORDER_CLEARANCE 1.03f" in temporal and "glDepthFunc(GL_ALWAYS); glDepthMask(GL_TRUE);" in underlay, "the second pass must win only against current faces that lie farther away, and leave depth alone", @@ -100,6 +101,11 @@ def shader(source: str, name: str) -> str: " : tifrom[ti],now,alpha,&out);\n" in present + and " if (now->depth[k]>0.0f) v[t?23:9]=temporal_order(gpu_temporal_lerp_depth(now,&out,k));\n" in present, + "a face between two frames must be ordered by the depth it has there, or a wall and its pieces part", +) require( " temporal_submit(s,c,s->height,s_temp_scratch,split);\n" " temporal_underlay(alpha,0);\n" diff --git a/tests/unit/gpu_temporal_match_test.cpp b/tests/unit/gpu_temporal_match_test.cpp index f3807c0..28d1fb1 100644 --- a/tests/unit/gpu_temporal_match_test.cpp +++ b/tests/unit/gpu_temporal_match_test.cpp @@ -139,6 +139,96 @@ void invalid_geometry() { "affine interpolation retains the zero-depth sentinel"); } +struct Point { + double x, y, z; +}; + +float screen_x(const Point &point) { return static_cast(160.0 + 160.0 * point.x / point.z); } +float screen_y(const Point &point) { return static_cast(120.0 + 160.0 * point.y / point.z); } + +GpuTemporalTriangle placed(const std::array &points) { + auto face = triangle(); + for (int vertex = 0; vertex < 3; ++vertex) { + face.x[vertex] = screen_x(points[vertex]); + face.y[vertex] = screen_y(points[vertex]); + face.depth[vertex] = static_cast(points[vertex].z); + } + return face; +} + +GpuTemporalTriangle weighted(const std::array &points, const std::array &other) { + auto face = placed(other); + for (int vertex = 0; vertex < 3; ++vertex) { + face.w[vertex] = static_cast(points[vertex].z / other[vertex].z); + face.x[vertex] = screen_x(points[vertex]) * face.w[vertex]; + face.y[vertex] = screen_y(points[vertex]) * face.w[vertex]; + } + return face; +} + +bool close(double value, double expected) { return std::fabs(value - expected) <= 0.001 * std::fabs(expected); } + +void interpolated_depth() { + const Point a_then{-50.0, 10.0, 600.0}, b_then{350.0, -20.0, 900.0}, c_then{-60.0, 110.0, 610.0}; + const Point a_now{-100.0, 0.0, 200.0}, b_now{300.0, 0.0, 800.0}, c_now{-100.0, 100.0, 200.0}; + const Point m_then{(a_then.x + b_then.x) / 2, (a_then.y + b_then.y) / 2, (a_then.z + b_then.z) / 2}; + const Point m_now{(a_now.x + b_now.x) / 2, (a_now.y + b_now.y) / 2, (a_now.z + b_now.z) / 2}; + const std::array wall_then{a_then, b_then, c_then}, wall_now{a_now, b_now, c_now}; + const std::array piece_then{a_then, m_then, c_then}, piece_now{a_now, m_now, c_now}; + const auto wall = placed(wall_now), piece = placed(piece_now); + const auto wall_from = weighted(wall_then, wall_now), piece_from = weighted(piece_then, piece_now); + GpuTemporalTriangle out{}, part{}; + for (const float alpha : {0.0f, 0.1f, 0.5f, 0.9f, 1.0f}) { + gpu_temporal_lerp(&wall_from, &wall, alpha, &out); + for (int vertex = 0; vertex < 3; ++vertex) + expect(close(gpu_temporal_lerp_depth(&wall, &out, vertex), + wall_then[vertex].z + (wall_now[vertex].z - wall_then[vertex].z) * alpha), + "a corner placed from its earlier weight is as deep as it is on the way"); + const auto start = placed(wall_then), gone_to = weighted(wall_now, wall_then); + auto end = gone_to; + for (int vertex = 0; vertex < 3; ++vertex) end.depth[vertex] = static_cast(wall_now[vertex].z); + gpu_temporal_lerp(&start, &end, alpha, &out); + for (int vertex = 0; vertex < 3; ++vertex) + expect(close(gpu_temporal_lerp_depth(&end, &out, vertex), + wall_then[vertex].z + (wall_now[vertex].z - wall_then[vertex].z) * alpha), + "a departed corner placed toward its later weight is as deep as it is on the way"); + } + gpu_temporal_lerp(&wall_from, &wall, 0.5f, &out); + gpu_temporal_lerp(&piece_from, &piece, 0.5f, &part); + const double edge_x = out.x[1] - out.x[0], edge_y = out.y[1] - out.y[0]; + const double along = ((part.x[1] - out.x[0]) * edge_x + (part.y[1] - out.y[0]) * edge_y) / (edge_x * edge_x + edge_y * edge_y); + const double off = (part.x[1] - out.x[0]) * edge_y - (part.y[1] - out.y[0]) * edge_x; + expect(std::fabs(off) < 0.01 * std::sqrt(edge_x * edge_x + edge_y * edge_y) && along > 0.0 && along < 1.0, + "a corner in the middle of a wall's edge stays on that edge between the frames"); + const double wall_there = 1.0 / ((1.0 - along) / gpu_temporal_lerp_depth(&wall, &out, 0) + + along / gpu_temporal_lerp_depth(&wall, &out, 1)); + expect(close(wall_there, gpu_temporal_lerp_depth(&piece, &part, 1)), + "a wall and the piece drawn over it are equally deep where they meet between the frames"); + const double stale = 1.0 / ((1.0 - along) / wall.depth[0] + along / wall.depth[1]); + expect(std::fabs(stale - piece.depth[1]) > 0.03 * piece.depth[1], + "the depths of the later frame alone would part the wall from its piece by more than the margin"); + const auto still = triangle(), moved = triangle(8.0f, -6.0f); + auto deep = moved; + for (float &depth : deep.depth) depth = 300.0f; + gpu_temporal_lerp(&still, &deep, 0.5f, &out); + expect(gpu_temporal_lerp_depth(&deep, &out, 0) == 300.0f, "a corner without a weight keeps the depth it ends at"); + auto behind = wall_from; + behind.w[0] = -0.5f; + gpu_temporal_lerp(&behind, &wall, 0.2f, &out); + expect(out.w[0] < 0.0f && gpu_temporal_lerp_depth(&wall, &out, 0) == wall.depth[0], + "a corner not yet in front of the viewer keeps the depth it ends at"); + behind.w[0] = -1.0f; + gpu_temporal_lerp(&behind, &wall, 0.5f, &out); + expect(out.w[0] < 0.0f && gpu_temporal_lerp_depth(&wall, &out, 0) == wall.depth[0], + "a weight that comes to nothing on the way is not read as a corner without a weight"); + auto leaving = placed(wall_then); + leaving.w[0] = -0.5f; + leaving.depth[0] = wall.depth[0] * leaving.w[0]; + gpu_temporal_lerp(&wall, &leaving, 0.2f, &out); + expect(close(gpu_temporal_lerp_depth(&leaving, &out, 0), wall.depth[0] + (leaving.depth[0] - wall.depth[0]) * 0.2), + "a departed corner on its way behind the viewer is as deep as it is on the way"); +} + void scale_and_fallback() { constexpr int count = GPU_TEMPORAL_MAX_TRIANGLES; std::vector previous, current; @@ -176,6 +266,7 @@ int main() { interpolation(); correspondence(); invalid_geometry(); + interpolated_depth(); scale_and_fallback(); if (failures) return 1; std::cout << "GPU temporal geometry matching tests passed\n"; diff --git a/tests/unit/gpu_temporal_mesh_test.cpp b/tests/unit/gpu_temporal_mesh_test.cpp index fbe891c..086f25a 100644 --- a/tests/unit/gpu_temporal_mesh_test.cpp +++ b/tests/unit/gpu_temporal_mesh_test.cpp @@ -902,6 +902,11 @@ void facing_and_camera_bounds() { auto ghost = current; ghost.w[0] = static_cast(GPU_TEMPORAL_MIN_WEIGHT) * 0.5f; expect(!gpu_temporal_mesh_facing(¤t, &ghost), "a corner not yet in front of the viewer hides its face"); + auto behind = current, crossing = current; + behind.w[0] = -1.0f; + gpu_temporal_lerp(&behind, ¤t, 0.5f, &crossing); + expect(crossing.w[0] < 0.0f && !gpu_temporal_mesh_facing(¤t, &crossing), + "a corner in the viewer's own plane hides its face, its weight of nothing is not a missing weight"); auto broken = current; broken.x[1] = std::numeric_limits::infinity(); expect(gpu_temporal_mesh_camera_safe(¤t, &flipped) && !gpu_temporal_mesh_camera_safe(¤t, &broken),