mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Merge pull request #716: post effects depth, history and live uniforms
Coherent projection publication and bounded diagnostics; repaired final-pass history advancement and reload clock. Real GL182 checks and combined title regression/visual gate passed. Owner explicitly authorized this merge.
This commit is contained in:
@@ -92,6 +92,19 @@ Bloom uses the mask at both ends: only lit pixels feed its bright pass, and
|
||||
only lit pixels take the glow back. HUD pixels are therefore never changed,
|
||||
even next to a lamp.
|
||||
|
||||
## Depth, history, time and live uniforms
|
||||
|
||||
- `input <sampler> depth`: view depth `sz` (GTE screen z) of the presented frame, R32F at window
|
||||
size, rebuilt from the surfaces' PGXP depth (`zn = 1 - 512/(sz + 256)`); 0 where nothing 3D drew.
|
||||
Only built while a loaded chain reads it.
|
||||
- `input <sampler> history`: this pass's own target from the last real frame (trails, temporal
|
||||
filters), including the final window pass. Generated frames do not update it. History starts empty after loading or resizing.
|
||||
- `u_time` (host seconds since the chain first drew, reset on reload) and `u_proj` (latest GTE projection: `240/H`, screen
|
||||
centre in uv, aspect) are declared in every pass.
|
||||
- Live uniforms: a title sets a preset `uniform` from game state with
|
||||
`psx_host_set_post_uniform(name, x, y, z, w)` (`host_overlay.h`), e.g. a light source projected
|
||||
to the screen; the preset value is the default until the first call.
|
||||
|
||||
## Tests
|
||||
|
||||
- `postfx_chain_test`: the format, built-in presets, errors and file import.
|
||||
@@ -102,3 +115,7 @@ even next to a lamp.
|
||||
thread and on the render thread.
|
||||
- `gl_post_aa_test`: FXAA on the pipeline. Its output is byte-identical to
|
||||
the former fixed pass.
|
||||
|
||||
The projection is a coherent latest CPU value. It is not associated with a
|
||||
queued frame or an individual replay view; shaders requiring an exact camera
|
||||
for a delayed or stereo image need corresponding frame/view metadata.
|
||||
|
||||
@@ -23,6 +23,9 @@ psxrecomp_add_runtime_target(psx-runtime
|
||||
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(postfx_publication_test tests/test_postfx_publication.cpp)
|
||||
target_include_directories(postfx_publication_test PRIVATE include)
|
||||
add_test(NAME postfx_publication_test COMMAND postfx_publication_test)
|
||||
# Windows UTF-8 text boundary (issue #371): the helpers legacy Windows
|
||||
# relies on, and — with the shipped manifest — a UTF-8 process code page.
|
||||
if(WIN32)
|
||||
|
||||
@@ -199,6 +199,8 @@ float gl_renderer_get_bloom(void);
|
||||
* compile is logged and skipped. */
|
||||
void gl_renderer_set_post_shader(const char *path);
|
||||
int gl_renderer_post_shader_loaded(void);
|
||||
/* Debug: path, last load outcome and live uniforms of the user chain (JSON members). */
|
||||
int gl_renderer_post_shader_debug(char *out, int cap);
|
||||
typedef struct {
|
||||
float strength, threshold;
|
||||
uint64_t passes, us_n;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
typedef struct GtePostfxProjection { int32_t h, ofx, ofy; } GtePostfxProjection;
|
||||
/* Coherent latest CPU projection, not a frame/view identity. One CPU producer. */
|
||||
void gte_postfx_projection_enable(int enabled);
|
||||
int gte_postfx_projection_read(GtePostfxProjection *out);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "gte_postfx_projection.h"
|
||||
#include <atomic>
|
||||
/* CPU owns cached fields; renderer owns enable(). Atomic payloads plus an SC
|
||||
* version guard make concurrent reads defined and reject mixed tuples. */
|
||||
class GteProjectionPublication {
|
||||
std::atomic<uint32_t> control{0}, version{0}, published{0};
|
||||
std::atomic<int32_t> h{0}, x{0}, y{0};
|
||||
uint32_t seen=0; GtePostfxProjection cached{};
|
||||
public:
|
||||
void enable(bool on) {
|
||||
const auto c=control.load();
|
||||
if(bool(c&1)!=on) control.store(((c+2)&~1u)|uint32_t(on));
|
||||
}
|
||||
void publish(int32_t ph,int32_t px,int32_t py) {
|
||||
const auto c=control.load(std::memory_order_relaxed);
|
||||
if(!(c&1))return;
|
||||
if(c==seen && ph==cached.h && px==cached.ofx && py==cached.ofy)return;
|
||||
version.fetch_add(1); h.store(ph); x.store(px); y.store(py); published.store(c);
|
||||
version.fetch_add(1); seen=c; cached={ph,px,py};
|
||||
}
|
||||
bool read(GtePostfxProjection& out) const {
|
||||
const auto c=control.load(); if(!(c&1))return false;
|
||||
for(unsigned i=0;i<3;++i) {
|
||||
const auto v=version.load(); if(v&1)continue;
|
||||
const auto p=published.load(); GtePostfxProjection value{h.load(),x.load(),y.load()};
|
||||
if(v==version.load() && p==c && c==control.load()) {out=value;return true;}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -56,6 +56,13 @@ void psx_host_overlay_draw(int width, int height); /* renderer side */
|
||||
void psx_host_set_hud_draw_cb(PsxHostOverlayDrawFn fn, void *ctx);
|
||||
void psx_host_hud_draw(int width, int height); /* renderer side */
|
||||
|
||||
/* ---- Post-effect live uniforms ------------------------------------------
|
||||
* Sets the value of a `uniform <name> ...` line of the loaded post-effect chain
|
||||
* ([video] post_shader, postfx_chain.h) from game state, e.g. a light source
|
||||
* projected to the screen. Any thread; effective at the next present; the
|
||||
* preset default applies until the first call. Up to 16 names. */
|
||||
void psx_host_set_post_uniform(const char *name, float x, float y, float z, float w);
|
||||
|
||||
/* ---- P3: input sink -----------------------------------------------------
|
||||
* The sink sees every host SDL event first (main thread), in the normal event
|
||||
* drain and in the host pause loop. Returning non-zero consumes the event.
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
* pfx 1 # format version (required first)
|
||||
* pass <name> # starts a pass; the last pass writes the window
|
||||
* shader <file.glsl|builtin:name> # fragment body (see below)
|
||||
* input <sampler> <source|source_down|mask|previous|<pass name>>
|
||||
* input <sampler> <source|source_down|mask|depth|history|previous|<pass name>>
|
||||
* # depth: PGXP view depth sz (GTE screen z, R32F;
|
||||
* # 0 where nothing 3D drew); history: this pass's
|
||||
* # own target from the last real frame (not last pass)
|
||||
* # source_down: the image scaled to this pass's
|
||||
* # target (one bilinear blit, no full-size copy)
|
||||
* scale <f> # target size, fraction of the game rect (default 1)
|
||||
@@ -27,6 +30,7 @@
|
||||
* uniform vec2 u_rcp; // 1 / this pass's target size in px
|
||||
* uniform vec2 u_size; // this pass's target size in px
|
||||
* uniform ivec2 u_org; // window origin of the game rect (gl_FragCoord - u_org = px in it)
|
||||
* uniform float u_time; // seconds since the chain loaded (real and generated frames)
|
||||
* plus one `uniform sampler2D <sampler>;` per input. v_uv is 0..1 over the
|
||||
* game rect, bottom-up like the window. */
|
||||
#ifndef PSX_POSTFX_CHAIN_H
|
||||
@@ -38,7 +42,8 @@ extern "C" {
|
||||
|
||||
enum { PFX_MAX_PASSES = 16, PFX_MAX_INPUTS = 4, PFX_MAX_UNIFORMS = 8, PFX_NAME = 32 };
|
||||
/* Input sources; a value >= 0 is an earlier pass's index. */
|
||||
enum { PFX_IN_SOURCE = -1, PFX_IN_MASK = -2, PFX_IN_SOURCE_DOWN = -3 };
|
||||
enum { PFX_IN_SOURCE = -1, PFX_IN_MASK = -2, PFX_IN_SOURCE_DOWN = -3, PFX_IN_DEPTH = -4,
|
||||
PFX_IN_HISTORY = -5 };
|
||||
enum { PFX_BLEND_REPLACE = 0, PFX_BLEND_SCREEN = 1 };
|
||||
enum { PFX_FMT_RGBA8 = 0, PFX_FMT_RGBA16F = 1 };
|
||||
|
||||
@@ -69,6 +74,7 @@ typedef struct {
|
||||
char name[PFX_NAME];
|
||||
int npasses;
|
||||
int uses_mask; /* some pass reads `mask` */
|
||||
int uses_depth; /* some pass reads `depth` */
|
||||
PfxPass pass[PFX_MAX_PASSES];
|
||||
} PfxChain;
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
typedef struct PfxDebugLive { char name[32]; float v[4]; } PfxDebugLive;
|
||||
typedef struct PfxJsonWriter { char *out; size_t cap,used; int ok; } PfxJsonWriter;
|
||||
static inline void pfx_json_raw(PfxJsonWriter *w,const char *s) {
|
||||
const size_t n=strlen(s);
|
||||
if(!w->ok || n>=w->cap-w->used){w->ok=0;return;}
|
||||
memcpy(w->out+w->used,s,n+1);w->used+=n;
|
||||
}
|
||||
static inline void pfx_json_quote(PfxJsonWriter *w,const char *s) {
|
||||
pfx_json_raw(w,"\"");
|
||||
for(const unsigned char *p=(const unsigned char*)s;*p;++p) {
|
||||
char b[7];
|
||||
if(*p=='"'||*p=='\\'){b[0]='\\';b[1]=(char)*p;b[2]=0;}
|
||||
else if(*p<32)snprintf(b,sizeof b,"\\u%04x",(unsigned)*p);
|
||||
else {b[0]=(char)*p;b[1]=0;}
|
||||
pfx_json_raw(w,b);
|
||||
}
|
||||
pfx_json_raw(w,"\"");
|
||||
}
|
||||
/* Returns 0 with an empty buffer on insufficient space, never partial JSON. */
|
||||
static inline int pfx_debug_json(char *out,int cap,const char *path,const char *status,
|
||||
const PfxDebugLive *live,int count) {
|
||||
if(!out||cap<=0)return 0;
|
||||
out[0]=0;PfxJsonWriter w={out,(size_t)cap,0,1};
|
||||
pfx_json_raw(&w,"\"path\":");pfx_json_quote(&w,path);
|
||||
pfx_json_raw(&w,",\"status\":");pfx_json_quote(&w,status);
|
||||
pfx_json_raw(&w,",\"uniforms\":{");
|
||||
for(int i=0;i<count;++i) {
|
||||
if(i)pfx_json_raw(&w,",");
|
||||
pfx_json_quote(&w,live[i].name);pfx_json_raw(&w,":[");
|
||||
for(int j=0;j<4;++j) {
|
||||
if(j)pfx_json_raw(&w,",");
|
||||
if(!isfinite(live[i].v[j])){w.ok=0;break;}
|
||||
char number[48];snprintf(number,sizeof number,"%.9g",(double)live[i].v[j]);pfx_json_raw(&w,number);
|
||||
}
|
||||
pfx_json_raw(&w,"]");
|
||||
}
|
||||
pfx_json_raw(&w,"}");
|
||||
if(!w.ok){out[0]=0;return 0;}return (int)w.used;
|
||||
}
|
||||
@@ -8778,6 +8778,19 @@ static void handle_bloom(int id, const char *json)
|
||||
bi.gen_us_mean, (unsigned long long)bi.reused);
|
||||
}
|
||||
|
||||
/* post_shader: {"cmd":"post_shader"} reads the user chain (path, load outcome, live
|
||||
* uniforms); "path" sets it, "" turns it off, the same path reloads it (shader edits). */
|
||||
static void handle_post_shader(int id, const char *json)
|
||||
{
|
||||
char path[512];
|
||||
if (json_get_str(json, "path", path, sizeof path)) gl_renderer_set_post_shader(path);
|
||||
char info[1536];
|
||||
if (!gl_renderer_post_shader_debug(info, (int)sizeof info)) {
|
||||
send_err(id, "post shader diagnostics exceed response capacity"); return;
|
||||
}
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"loaded\":%d,%s}", id, gl_renderer_post_shader_loaded(), info);
|
||||
}
|
||||
|
||||
static void handle_post_aa(int id, const char *json)
|
||||
{
|
||||
int mode = json_get_int(json, "mode", -1);
|
||||
@@ -16413,6 +16426,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "frame_perf", handle_frame_perf },
|
||||
{ "batch_sites", handle_batch_sites },
|
||||
{ "post_aa", handle_post_aa },
|
||||
{ "post_shader", handle_post_shader },
|
||||
{ "fmv_chroma", handle_fmv_chroma },
|
||||
{ "texture_lod", handle_texture_lod },
|
||||
{ "accurate_blending", handle_accurate_blending },
|
||||
|
||||
+224
-11
@@ -1,3 +1,8 @@
|
||||
#include "gte_postfx_projection.h"
|
||||
#include "postfx_debug_json.h"
|
||||
#ifndef PFX_NOW_TICKS
|
||||
#define PFX_NOW_TICKS() SDL_GetPerformanceCounter()
|
||||
#endif
|
||||
/* ---- Post-effect pipeline (docs/POST_EFFECTS.md) ---------------------------
|
||||
* One executor for every post effect on the composed game image: chains of
|
||||
* full-screen fragment passes (postfx_chain.h). Three stages, in order, each
|
||||
@@ -36,16 +41,20 @@ typedef struct {
|
||||
GLint fixed[PFX_LOC_FIXED];
|
||||
GLint in[PFX_MAX_INPUTS];
|
||||
GLint u[PFX_MAX_UNIFORMS];
|
||||
GLint time, proj;
|
||||
} PfxLocs;
|
||||
typedef struct {
|
||||
PfxChain chain;
|
||||
int loaded, failed;
|
||||
uint64_t started_at;
|
||||
GLuint prog[PFX_MAX_PASSES];
|
||||
PfxLocs loc[PFX_MAX_PASSES];
|
||||
GLuint tex[PFX_MAX_PASSES], fbo[PFX_MAX_PASSES];
|
||||
GLuint dtex[PFX_MAX_PASSES], dfbo[PFX_MAX_PASSES]; /* source_down copies */
|
||||
int dw[PFX_MAX_PASSES], dh[PFX_MAX_PASSES];
|
||||
int tw[PFX_MAX_PASSES], th[PFX_MAX_PASSES];
|
||||
GLuint htex[PFX_MAX_PASSES], hfbo[PFX_MAX_PASSES]; /* `history` copies */
|
||||
int hw[PFX_MAX_PASSES], hh[PFX_MAX_PASSES], hist[PFX_MAX_PASSES];
|
||||
} PfxStage;
|
||||
enum { PFX_STAGE_BLOOM, PFX_STAGE_USER, PFX_STAGE_AA, PFX_STAGE_HQ, PFX_NSTAGES };
|
||||
static PfxStage s_pfx[PFX_NSTAGES];
|
||||
@@ -78,6 +87,49 @@ static GLint s_mmap_uUv = -1, s_mmap_uS = -1, s_mmap_uSrc = -1, s_mmap_uDst = -
|
||||
* depth-stencil blit is needed (one costs the CPU milliseconds on macOS). */
|
||||
typedef struct { GLuint rb, tex, fbo; int w, h; } PfxSurfMask;
|
||||
static PfxSurfMask s_smask[2];
|
||||
/* `depth`: each surface's depth-stencil renderbuffer is blitted into a depth
|
||||
* texture of its size (dtex/dfbo), then mapped to the window as view depth sz
|
||||
* (GTE screen z; PGXP writes zn = 1 - 512/(sz + 256)) in an R32F texture. */
|
||||
typedef struct { GLuint rb, dtex, dfbo; int w, h; } PfxSurfDepth;
|
||||
static PfxSurfDepth s_sdepth[2];
|
||||
static GLuint s_depth_tex = 0, s_depth_fbo = 0, s_depth_map_prog = 0;
|
||||
static GLint s_dmap_uUv = -1, s_dmap_uS = -1, s_dmap_uSrc = -1, s_dmap_uDst = -1;
|
||||
static int s_depth_w = 0, s_depth_h = 0, s_depth_ok = 0;
|
||||
static int s_pfx_depth_on = 0; /* a loaded chain reads `depth` */
|
||||
/* Live values for preset `uniform` lines, set by the title from game state
|
||||
* (psx_host_set_post_uniform, host_overlay.h): a live value replaces the
|
||||
* preset default of the same name in every pass that declares it. */
|
||||
enum { PFX_LIVE_MAX = 16 };
|
||||
typedef PfxDebugLive PfxLive;
|
||||
static PfxLive s_pfx_live[PFX_LIVE_MAX];
|
||||
static int s_pfx_nlive = 0;
|
||||
static SDL_SpinLock s_pfx_live_lock = 0;
|
||||
void psx_host_set_post_uniform(const char *name, float x, float y, float z, float w) {
|
||||
if (!name || !name[0] || strlen(name) >= sizeof s_pfx_live[0].name ||
|
||||
!isfinite(x) || !isfinite(y) || !isfinite(z) || !isfinite(w)) return;
|
||||
SDL_LockSpinlock(&s_pfx_live_lock);
|
||||
int i = 0;
|
||||
while (i < s_pfx_nlive && strcmp(s_pfx_live[i].name, name)) i++;
|
||||
if (i == s_pfx_nlive && s_pfx_nlive < PFX_LIVE_MAX) {
|
||||
snprintf(s_pfx_live[i].name, sizeof s_pfx_live[i].name, "%s", name);
|
||||
s_pfx_nlive++;
|
||||
}
|
||||
if (i < s_pfx_nlive) {
|
||||
s_pfx_live[i].v[0] = x; s_pfx_live[i].v[1] = y; s_pfx_live[i].v[2] = z; s_pfx_live[i].v[3] = w;
|
||||
}
|
||||
SDL_UnlockSpinlock(&s_pfx_live_lock);
|
||||
}
|
||||
/* Last load outcome of the user chain, for the debug server. */
|
||||
static char s_pfx_user_status[192] = "";
|
||||
static int pfx_live_get(const char *name, float out[4]) {
|
||||
int found = 0;
|
||||
SDL_LockSpinlock(&s_pfx_live_lock);
|
||||
for (int i = 0; i < s_pfx_nlive; i++)
|
||||
if (!strcmp(s_pfx_live[i].name, name)) { memcpy(out, s_pfx_live[i].v, sizeof s_pfx_live[i].v); found = 1; break; }
|
||||
SDL_UnlockSpinlock(&s_pfx_live_lock);
|
||||
return found;
|
||||
}
|
||||
|
||||
static int s_mask_w = 0, s_mask_h = 0;
|
||||
|
||||
static int pfx_stage_on(int i) {
|
||||
@@ -102,22 +154,30 @@ static void pfx_stage_unload(PfxStage *st) {
|
||||
if (st->tex[i]) glDeleteTextures(1, &st->tex[i]);
|
||||
if (st->dfbo[i]) p_glDeleteFramebuffers(1, &st->dfbo[i]);
|
||||
if (st->dtex[i]) glDeleteTextures(1, &st->dtex[i]);
|
||||
if (st->hfbo[i]) p_glDeleteFramebuffers(1, &st->hfbo[i]);
|
||||
if (st->htex[i]) glDeleteTextures(1, &st->htex[i]);
|
||||
}
|
||||
st->htex[i] = st->hfbo[i] = 0; st->hw[i] = st->hh[i] = 0; st->hist[i] = 0;
|
||||
st->prog[i] = st->tex[i] = st->fbo[i] = 0; st->tw[i] = st->th[i] = 0;
|
||||
st->dtex[i] = st->dfbo[i] = 0; st->dw[i] = st->dh[i] = 0;
|
||||
}
|
||||
pfx_chain_free(&st->chain);
|
||||
st->loaded = st->failed = 0;
|
||||
st->started_at = 0;
|
||||
}
|
||||
|
||||
/* Which stages read the mask decides whether draws keep it. */
|
||||
static void pfx_mask_wanted_update(void) {
|
||||
gte_postfx_projection_enable(postfx_active());
|
||||
int want = 0;
|
||||
for (int i = 0; i < PFX_NSTAGES; i++) {
|
||||
if (!pfx_stage_on(i)) continue;
|
||||
if (i == PFX_STAGE_BLOOM) want = 1; /* the bloom preset reads it */
|
||||
else if (s_pfx[i].loaded && s_pfx[i].chain.uses_mask) want = 1;
|
||||
}
|
||||
s_pfx_depth_on = 0;
|
||||
for (int i = 0; i < PFX_NSTAGES; i++)
|
||||
if (pfx_stage_on(i) && s_pfx[i].loaded && s_pfx[i].chain.uses_depth) s_pfx_depth_on = 1;
|
||||
if (want != s_bcls_on) { s_bcls_on = want; pgxp_render_wanted_update(); }
|
||||
}
|
||||
|
||||
@@ -135,26 +195,41 @@ static int pfx_stage_load(int i) {
|
||||
if (!ok) {
|
||||
fprintf(stdout, "psxrecomp: post effect '%s' not loaded: %s\n",
|
||||
i == PFX_STAGE_USER ? s_pfx_user_path : "built-in", err);
|
||||
if (i == PFX_STAGE_USER) snprintf(s_pfx_user_status, sizeof s_pfx_user_status, "not loaded: %s", err);
|
||||
st->failed = 1;
|
||||
return 0;
|
||||
}
|
||||
st->loaded = 1;
|
||||
if (i == PFX_STAGE_USER)
|
||||
for (int p = 0; p < st->chain.npasses; p++)
|
||||
for (int k = 0; k < st->chain.pass[p].ninputs; k++)
|
||||
if (st->chain.pass[p].in[k].from == PFX_IN_HISTORY) st->hist[p] = 1;
|
||||
if (i == PFX_STAGE_USER) {
|
||||
fprintf(stdout, "psxrecomp: post shader '%s' (%d passes%s)\n", s_pfx_user_path,
|
||||
st->chain.npasses, st->chain.uses_mask ? ", mask" : "");
|
||||
snprintf(s_pfx_user_status, sizeof s_pfx_user_status, "parsed: %d passes", st->chain.npasses);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint64_t s_pfx_builds = 0; /* program builds (tests) */
|
||||
static GLuint pfx_build_pass(const PfxPass *ps) {
|
||||
s_pfx_builds++;
|
||||
size_t n = strlen(ps->shader) + 512 + (size_t)ps->ninputs * 64;
|
||||
size_t n = strlen(ps->shader) + 512 + (size_t)ps->ninputs * 64 + (size_t)ps->nuniforms * 64;
|
||||
char *src = (char *)malloc(n);
|
||||
if (!src) return 0;
|
||||
int k = snprintf(src, n, "#version 330\nin vec2 v_uv; out vec4 o;\n"
|
||||
"uniform vec2 u_rcp; uniform vec2 u_size; uniform ivec2 u_org;\n");
|
||||
"uniform vec2 u_rcp; uniform vec2 u_size; uniform ivec2 u_org;\n"
|
||||
"uniform float u_time; uniform vec4 u_proj;\n");
|
||||
for (int i = 0; i < ps->ninputs; i++)
|
||||
k += snprintf(src + k, n - (size_t)k, "uniform sampler2D %s;\n", ps->in[i].sampler);
|
||||
/* The preset's `uniform` lines are declared here unless the body declares
|
||||
* them itself (the built-in shaders do). */
|
||||
static const char *const kTypes[] = { "float", "vec2", "vec3", "vec4" };
|
||||
for (int i = 0; i < ps->nuniforms && (size_t)k + 96 < n; i++) {
|
||||
char decl[PFX_NAME + 16];
|
||||
snprintf(decl, sizeof decl, "%s %s;", kTypes[ps->u[i].count - 1], ps->u[i].name);
|
||||
if (!strstr(ps->shader, decl)) k += snprintf(src + k, n - (size_t)k, "uniform %s\n", decl);
|
||||
}
|
||||
snprintf(src + k, n - (size_t)k, "%s", ps->shader);
|
||||
GLuint prog = build_program(PRESENT_VS, src);
|
||||
free(src);
|
||||
@@ -188,7 +263,7 @@ static void pfx_rect_px(GLuint tex, float tex_w, float tex_h, int x, int y, int
|
||||
static void postfx_note_center(GLuint fbo, GLuint tex, float tex_w, float tex_h,
|
||||
int x, int y, int w, int h, int off, int wide_w) {
|
||||
s_mask_ctr_valid = 0;
|
||||
if (!s_bcls_on || !fbo || !tex || wide_w <= 0) return;
|
||||
if ((!s_bcls_on && !s_pfx_depth_on) || !fbo || !tex || wide_w <= 0) return;
|
||||
s_mask_ctr_rb = pfx_surface_rb(fbo);
|
||||
if (!s_mask_ctr_rb) return;
|
||||
pfx_rect_px(tex, tex_w, tex_h, x, y, w, h, s_mask_ctr, &s_mask_ctr_tw, &s_mask_ctr_th);
|
||||
@@ -204,7 +279,7 @@ static void postfx_note_source(GLuint fbo, GLuint tex, float tex_w, float tex_h,
|
||||
int x, int y, int w, int h) {
|
||||
s_mask_src_valid = 0;
|
||||
s_mask_ctr_valid = 0;
|
||||
if (!s_bcls_on || !fbo || !tex || tex_w <= 0.0f || tex_h <= 0.0f) return;
|
||||
if ((!s_bcls_on && !s_pfx_depth_on) || !fbo || !tex || tex_w <= 0.0f || tex_h <= 0.0f) return;
|
||||
s_mask_src_rb = pfx_surface_rb(fbo);
|
||||
if (!s_mask_src_rb) return;
|
||||
pfx_rect_px(tex, tex_w, tex_h, x, y, w, h, s_mask_src, &s_mask_src_tw, &s_mask_src_th);
|
||||
@@ -217,6 +292,7 @@ static void pfx_tex_alloc(GLuint *t, int w, int h, int fmt, int nearest) {
|
||||
glBindTexture(GL_TEXTURE_2D, *t);
|
||||
if (fmt == PFX_FMT_RGBA16F) glTexImage2D(GL_TEXTURE_2D, 0, 0x881A /*RGBA16F*/, w, h, 0, GL_RGBA, GL_FLOAT, NULL);
|
||||
else if (fmt == -1) glTexImage2D(GL_TEXTURE_2D, 0, 0x8229 /*R8*/, w, h, 0, 0x1903 /*RED*/, GL_UNSIGNED_BYTE, NULL);
|
||||
else if (fmt == -2) glTexImage2D(GL_TEXTURE_2D, 0, 0x822E /*R32F*/, w, h, 0, 0x1903 /*RED*/, GL_FLOAT, NULL);
|
||||
else glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
@@ -309,6 +385,81 @@ static int pfx_mask_build(int fresh, int lw, int lh) {
|
||||
return s_mask_ok;
|
||||
}
|
||||
|
||||
/* Slot k: surface rb (tw x th) blitted into its depth texture. */
|
||||
static int pfx_surf_depth(int k, GLuint fbo, GLuint rb, int tw, int th) {
|
||||
PfxSurfDepth *d = &s_sdepth[k];
|
||||
if (d->rb != rb || d->w != tw || d->h != th) {
|
||||
if (!d->dtex) glGenTextures(1, &d->dtex);
|
||||
glBindTexture(GL_TEXTURE_2D, d->dtex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, PSXGL_DEPTH24_STENCIL8, tw, th, 0,
|
||||
0x84F9 /*DEPTH_STENCIL*/, 0x84FA /*UNSIGNED_INT_24_8*/, NULL);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
if (!d->dfbo) p_glGenFramebuffers(1, &d->dfbo);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, d->dfbo);
|
||||
p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, d->dtex, 0);
|
||||
d->rb = rb; d->w = tw; d->h = th;
|
||||
}
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, d->dfbo);
|
||||
p_glBlitFramebuffer(0, 0, tw, th, 0, 0, tw, th, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
return 1;
|
||||
}
|
||||
static void pfx_depth_map(int k, const int r[4], int x0, int x1, int lh) {
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_depth_fbo);
|
||||
glViewport(x0, 0, x1 - x0, lh);
|
||||
p_glUseProgram(s_depth_map_prog);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, s_sdepth[k].dtex);
|
||||
p_glUniform1i(s_dmap_uS, 0);
|
||||
p_glUniform4i(s_dmap_uSrc, r[0], r[1], r[2], r[3]);
|
||||
p_glUniform4f(s_dmap_uDst, (float)x0, (float)(x1 - x0), (float)lh, 0.0f);
|
||||
post_fullscreen(s_depth_map_prog, s_dmap_uUv);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
/* Window-sized view depth from the presented surface (fresh) or the last. */
|
||||
static int pfx_depth_build(int fresh, int lw, int lh) {
|
||||
if (!s_depth_map_prog) {
|
||||
s_depth_map_prog = build_program(PRESENT_VS,
|
||||
"#version 330\nin vec2 v_uv; out vec4 o; uniform sampler2D u_s;\n"
|
||||
"uniform ivec4 u_src; uniform vec4 u_dst;\n"
|
||||
"void main(){\n"
|
||||
" vec2 f = gl_FragCoord.xy;\n"
|
||||
" int sx = u_src.x + int(floor((f.x - u_dst.x) * float(u_src.z - u_src.x) / u_dst.y));\n"
|
||||
" int sy = u_src.y + int(floor((u_dst.z - f.y) * float(u_src.w - u_src.y) / u_dst.z));\n"
|
||||
" float d = texelFetch(u_s, ivec2(sx, sy), 0).r;\n"
|
||||
" /* zn = 2d - 1 = 1 - 512/(sz + 256); d = 0.5 is the zn = 0 of 2D draws */\n"
|
||||
" float sz = (d >= 0.999999 || abs(d - 0.5) < 1e-6) ? 0.0 : 256.0 / max(1.0 - d, 1e-6) - 256.0;\n"
|
||||
" o = vec4(sz);\n"
|
||||
"}\n");
|
||||
if (!s_depth_map_prog) return 0;
|
||||
s_dmap_uUv = p_glGetUniformLocation(s_depth_map_prog, "u_uv_rect");
|
||||
s_dmap_uS = p_glGetUniformLocation(s_depth_map_prog, "u_s");
|
||||
s_dmap_uSrc = p_glGetUniformLocation(s_depth_map_prog, "u_src");
|
||||
s_dmap_uDst = p_glGetUniformLocation(s_depth_map_prog, "u_dst");
|
||||
}
|
||||
if (s_depth_w != lw || s_depth_h != lh) {
|
||||
pfx_tex_alloc(&s_depth_tex, lw, lh, -2, 1);
|
||||
pfx_fbo_attach(&s_depth_fbo, s_depth_tex);
|
||||
s_depth_w = lw; s_depth_h = lh; s_depth_ok = 0;
|
||||
}
|
||||
if (fresh && s_mask_src_valid) {
|
||||
pfx_surf_depth(0, s_mask_src_fbo, s_mask_src_rb, s_mask_src_tw, s_mask_src_th);
|
||||
if (s_mask_ctr_valid)
|
||||
pfx_surf_depth(1, s_mask_ctr_fbo, s_mask_ctr_rb, s_mask_ctr_tw, s_mask_ctr_th);
|
||||
pfx_depth_map(0, s_mask_src, 0, lw, lh);
|
||||
if (s_mask_ctr_valid) {
|
||||
const int c0 = (int)lrintf(s_mask_ctr_x0 * (float)lw), c1 = (int)lrintf(s_mask_ctr_x1 * (float)lw);
|
||||
if (c1 > c0) pfx_depth_map(1, s_mask_ctr, c0, c1, lh);
|
||||
}
|
||||
s_depth_ok = 1;
|
||||
}
|
||||
return s_depth_ok;
|
||||
}
|
||||
|
||||
/* Run one stage's chain over the window rect. 0 when it could not run.
|
||||
* generated: a Smooth motion in-between frame; passes marked `generated
|
||||
* reuse` keep the target the last real frame left (when it is there at this
|
||||
@@ -318,6 +469,7 @@ static int pfx_mask_build(int fresh, int lw, int lh) {
|
||||
static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, int lh) {
|
||||
PfxChain *c = &st->chain;
|
||||
int copied = 0;
|
||||
if (!st->started_at) st->started_at = PFX_NOW_TICKS();
|
||||
for (int i = 0; i < c->npasses; i++) {
|
||||
const PfxPass *ps = &c->pass[i];
|
||||
const int last = i == c->npasses - 1;
|
||||
@@ -330,12 +482,16 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in
|
||||
L->fixed[PFX_LOC_SIZE] = p_glGetUniformLocation(pg, "u_size");
|
||||
L->fixed[PFX_LOC_ORG] = p_glGetUniformLocation(pg, "u_org");
|
||||
L->fixed[PFX_LOC_UV] = p_glGetUniformLocation(pg, "u_uv_rect");
|
||||
L->time = p_glGetUniformLocation(pg, "u_time");
|
||||
L->proj = p_glGetUniformLocation(pg, "u_proj");
|
||||
for (int k = 0; k < ps->ninputs; k++) L->in[k] = p_glGetUniformLocation(pg, ps->in[k].sampler);
|
||||
for (int k = 0; k < ps->nuniforms; k++) L->u[k] = p_glGetUniformLocation(pg, ps->u[k].name);
|
||||
}
|
||||
if (!st->prog[i]) {
|
||||
fprintf(stdout, "psxrecomp: post effect '%s' pass '%s' did not compile; effect off\n",
|
||||
c->name, ps->name);
|
||||
if (st == &s_pfx[PFX_STAGE_USER])
|
||||
snprintf(s_pfx_user_status, sizeof s_pfx_user_status, "pass '%s' did not compile", ps->name);
|
||||
st->failed = 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -372,6 +528,17 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in
|
||||
p_glBlitFramebuffer(lx, ly, lx + lw, ly + lh, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
}
|
||||
}
|
||||
/* Allocate/reset before sampling, including the final window pass.
|
||||
* A resize starts an empty history rather than sampling the old size. */
|
||||
if (st->hist[i] && (st->hw[i] != w || st->hh[i] != h)) {
|
||||
pfx_tex_alloc(&st->htex[i], w, h, ps->format, ps->nearest);
|
||||
pfx_fbo_attach(&st->hfbo[i], st->htex[i]);
|
||||
st->hw[i] = w; st->hh[i] = h;
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, st->hfbo[i]);
|
||||
GLfloat clear[4]; glGetFloatv(GL_COLOR_CLEAR_VALUE, clear);
|
||||
glClearColor(0,0,0,0); glClear(GL_COLOR_BUFFER_BIT);
|
||||
glClearColor(clear[0],clear[1],clear[2],clear[3]);
|
||||
}
|
||||
if (last) { p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); glViewport(lx, ly, lw, lh); }
|
||||
else { p_glBindFramebuffer(PSXGL_FRAMEBUFFER, st->fbo[i]); glViewport(0, 0, w, h); }
|
||||
const GLuint prog = st->prog[i];
|
||||
@@ -381,7 +548,9 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in
|
||||
const int from = ps->in[k].from;
|
||||
const GLuint t = from == PFX_IN_SOURCE ? s_post_copy_tex
|
||||
: from == PFX_IN_SOURCE_DOWN ? st->dtex[i]
|
||||
: from == PFX_IN_MASK ? s_mask_tex : st->tex[from];
|
||||
: from == PFX_IN_MASK ? s_mask_tex
|
||||
: from == PFX_IN_DEPTH ? s_depth_tex
|
||||
: from == PFX_IN_HISTORY ? st->htex[i] : st->tex[from];
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + (GLenum)k);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
p_glUniform1i(L->in[k], k);
|
||||
@@ -390,15 +559,31 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in
|
||||
p_glUniform2f(L->fixed[PFX_LOC_RCP], 1.0f / (float)w, 1.0f / (float)h);
|
||||
p_glUniform2f(L->fixed[PFX_LOC_SIZE], (float)w, (float)h);
|
||||
p_glUniform2i(L->fixed[PFX_LOC_ORG], last ? lx : 0, last ? ly : 0);
|
||||
if (L->proj >= 0) {
|
||||
/* u_proj: x/z = (uv.x - y) * w * x, y/z = (uv.y - z) * x for a view-space
|
||||
* point at depth sz (240-line reference, same scale on both axes). */
|
||||
const float aspect = (float)lw / (float)(lh > 0 ? lh : 1);
|
||||
GtePostfxProjection projection = {256, 160 * 65536, 120 * 65536};
|
||||
(void)gte_postfx_projection_read(&projection);
|
||||
const float H = projection.h > 0 ? (float)projection.h : 256.0f;
|
||||
const float ofx = (float)projection.ofx / 65536.0f, ofy = (float)projection.ofy / 65536.0f;
|
||||
p_glUniform4f(L->proj, 240.0f / H, 0.5f + (ofx - 160.0f) / (240.0f * aspect),
|
||||
1.0f - (ofy > 0.0f ? ofy : 120.0f) / 240.0f, aspect);
|
||||
}
|
||||
if (L->time >= 0)
|
||||
p_glUniform1f(L->time, (float)((double)(PFX_NOW_TICKS() - st->started_at) /
|
||||
(double)SDL_GetPerformanceFrequency()));
|
||||
for (int k = 0; k < ps->nuniforms; k++) {
|
||||
const PfxUniform *u = &ps->u[k];
|
||||
const GLint loc = L->u[k];
|
||||
if (loc < 0) continue;
|
||||
float live[4];
|
||||
const float *v = pfx_live_get(u->name, live) ? live : u->v;
|
||||
switch (u->count) {
|
||||
case 1: p_glUniform1f(loc, u->v[0]); break;
|
||||
case 2: p_glUniform2f(loc, u->v[0], u->v[1]); break;
|
||||
case 3: p_glUniform3f(loc, u->v[0], u->v[1], u->v[2]); break;
|
||||
default: p_glUniform4f(loc, u->v[0], u->v[1], u->v[2], u->v[3]); break;
|
||||
case 1: p_glUniform1f(loc, v[0]); break;
|
||||
case 2: p_glUniform2f(loc, v[0], v[1]); break;
|
||||
case 3: p_glUniform3f(loc, v[0], v[1], v[2]); break;
|
||||
default: p_glUniform4f(loc, v[0], v[1], v[2], v[3]); break;
|
||||
}
|
||||
}
|
||||
if (ps->blend == PFX_BLEND_SCREEN) {
|
||||
@@ -408,6 +593,13 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in
|
||||
}
|
||||
post_fullscreen(prog, L->fixed[PFX_LOC_UV]);
|
||||
if (ps->blend != PFX_BLEND_REPLACE) glDisable(GL_BLEND);
|
||||
if (!generated && st->hist[i]) {
|
||||
/* Capture the final window target as well as intermediate FBOs. */
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, last ? 0 : st->fbo[i]);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, st->hfbo[i]);
|
||||
const int x = last ? lx : 0, y = last ? ly : 0;
|
||||
p_glBlitFramebuffer(x, y, x+w, y+h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
}
|
||||
}
|
||||
for (int k = PFX_MAX_INPUTS - 1; k >= 0; k--) {
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + (GLenum)k);
|
||||
@@ -453,7 +645,7 @@ static void postfx_present(int fresh, int with_aa, int lx, int ly, int lw, int l
|
||||
PfxGlState saved;
|
||||
pfx_state_save(&saved);
|
||||
glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDisable(GL_SCISSOR_TEST); glDisable(GL_STENCIL_TEST);
|
||||
int mask_done = 0, mask_ok = 0;
|
||||
int mask_done = 0, mask_ok = 0, depth_done = 0, depth_ok = 0;
|
||||
for (int i = 0; i < PFX_NSTAGES; i++) {
|
||||
if (!pfx_stage_on(i)) continue;
|
||||
if ((i == PFX_STAGE_AA || i == PFX_STAGE_HQ) && !with_aa) continue;
|
||||
@@ -463,6 +655,10 @@ static void postfx_present(int fresh, int with_aa, int lx, int ly, int lw, int l
|
||||
if (!mask_done) { mask_ok = pfx_mask_build(src_fresh, lw, lh); mask_done = 1; }
|
||||
if (!mask_ok) continue; /* no marks yet */
|
||||
}
|
||||
if (st->chain.uses_depth) {
|
||||
if (!depth_done) { depth_ok = pfx_depth_build(src_fresh, lw, lh); depth_done = 1; }
|
||||
if (!depth_ok) continue; /* no depth yet */
|
||||
}
|
||||
if (i == PFX_STAGE_BLOOM) {
|
||||
const float k = s_bloom;
|
||||
pfx_chain_set_uniform(&st->chain, "u_strength", 1, &k);
|
||||
@@ -505,6 +701,7 @@ static void postfx_present(int fresh, int with_aa, int lx, int ly, int lw, int l
|
||||
|
||||
/* Objects died with the context. */
|
||||
static void postfx_release(void) {
|
||||
gte_postfx_projection_enable(0);
|
||||
for (int i = 0; i < PFX_NSTAGES; i++) {
|
||||
for (int k = 0; k < PFX_MAX_PASSES; k++) {
|
||||
s_pfx[i].prog[k] = s_pfx[i].tex[k] = s_pfx[i].fbo[k] = 0;
|
||||
@@ -515,6 +712,12 @@ static void postfx_release(void) {
|
||||
}
|
||||
s_mask_tex = s_mask_fbo = s_mask_prog = s_mask_map_prog = 0;
|
||||
memset(s_smask, 0, sizeof s_smask);
|
||||
s_depth_tex = s_depth_fbo = s_depth_map_prog = 0; s_depth_w = s_depth_h = 0; s_depth_ok = 0;
|
||||
memset(s_sdepth, 0, sizeof s_sdepth);
|
||||
for (int i = 0; i < PFX_NSTAGES; i++)
|
||||
for (int k = 0; k < PFX_MAX_PASSES; k++) {
|
||||
s_pfx[i].htex[k] = s_pfx[i].hfbo[k] = 0; s_pfx[i].hw[k] = s_pfx[i].hh[k] = 0;
|
||||
}
|
||||
s_mask_w = s_mask_h = 0; s_mask_ok = 0; s_mask_src_valid = 0;
|
||||
}
|
||||
|
||||
@@ -538,6 +741,16 @@ void gl_renderer_set_post_shader(const char *path) {
|
||||
}
|
||||
pfx_mask_wanted_update();
|
||||
}
|
||||
/* Debug: the user chain's last load outcome and the live uniforms, as JSON members. */
|
||||
int gl_renderer_post_shader_debug(char *out, int cap) {
|
||||
GL_RT_SYNC("post_shader_debug");
|
||||
PfxLive live[PFX_LIVE_MAX]; int count;
|
||||
SDL_LockSpinlock(&s_pfx_live_lock);
|
||||
count = s_pfx_nlive; memcpy(live,s_pfx_live,sizeof live);
|
||||
SDL_UnlockSpinlock(&s_pfx_live_lock);
|
||||
return pfx_debug_json(out,cap,s_pfx_user_path,s_pfx_user_status,live,count);
|
||||
}
|
||||
|
||||
int gl_renderer_post_shader_loaded(void) {
|
||||
GL_RT_SYNC("post_shader");
|
||||
return s_pfx[PFX_STAGE_USER].loaded && !s_pfx[PFX_STAGE_USER].failed;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <cmath>
|
||||
#include "gte.h"
|
||||
#include "gte_projection_publication.hpp"
|
||||
#include "gte_view.h"
|
||||
#include "cpu_state.h"
|
||||
#include "execution_profile.h"
|
||||
@@ -20,6 +21,12 @@ extern "C" int gpu_ws_precise_nclip_enabled(void);
|
||||
* plus native replay of one candidate) is running. */
|
||||
extern "C" int psx_overlay_shadow_diff_active(void);
|
||||
extern "C" uint64_t s_frame_count; /* debug_server.c */
|
||||
/* Latest projection (H, OFX, OFY in 16.16), read by the post-effect executor. */
|
||||
static GteProjectionPublication s_postfx_projection;
|
||||
extern "C" void gte_postfx_projection_enable(int enabled) { s_postfx_projection.enable(enabled!=0); }
|
||||
extern "C" int gte_postfx_projection_read(GtePostfxProjection* out) {
|
||||
return out && s_postfx_projection.read(*out);
|
||||
}
|
||||
extern "C" uint32_t g_debug_last_store_pc; /* debug_server.c */
|
||||
extern "C" { int (*g_psx_projection_command)(CPUState*, uint32_t) = nullptr; }
|
||||
|
||||
@@ -1206,6 +1213,9 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
}
|
||||
}
|
||||
dome_probe_note(gte->SZ[3]); /* locate the dome draw fn (far-vertex tally) */
|
||||
if (!s_gte_replay_sandbox) { /* projection of the latest 3D draw, for post effects (u_proj) */
|
||||
s_postfx_projection.publish(gte->H,gte->OFX,gte->OFY);
|
||||
}
|
||||
int64_t sx16 = gte->OFX + xterm;
|
||||
int64_t sy16 = gte->OFY + (int64_t)gte->IR2 * h_div_sz;
|
||||
/* Render-view projection override (psx_mod_render_view, projection=1).
|
||||
|
||||
@@ -238,13 +238,15 @@ int pfx_chain_parse(const char *text, const char *name, PfxShaderLoader load, vo
|
||||
if (!strcmp(f, "source")) from = PFX_IN_SOURCE;
|
||||
else if (!strcmp(f, "source_down")) from = PFX_IN_SOURCE_DOWN;
|
||||
else if (!strcmp(f, "mask")) { from = PFX_IN_MASK; out->uses_mask = 1; }
|
||||
else if (!strcmp(f, "depth")) { from = PFX_IN_DEPTH; out->uses_depth = 1; }
|
||||
else if (!strcmp(f, "history")) from = PFX_IN_HISTORY;
|
||||
else if (!strcmp(f, "previous")) {
|
||||
if (self == 0) { pfx_chain_free(out); return pfx_fail(err, errlen, line, "the first pass has no previous"); }
|
||||
from = self - 1;
|
||||
} else {
|
||||
from = -100;
|
||||
for (int i = 0; i < self; i++) if (!strcmp(out->pass[i].name, f)) from = i;
|
||||
if (from == -100) { pfx_chain_free(out); return pfx_fail(err, errlen, line, "unknown input '%s' (source, mask, previous or an earlier pass)", f); }
|
||||
if (from == -100) { pfx_chain_free(out); return pfx_fail(err, errlen, line, "unknown input '%s' (source, mask, depth, history, previous or an earlier pass)", f); }
|
||||
}
|
||||
snprintf(cur->in[cur->ninputs].sampler, PFX_NAME, "%s", s);
|
||||
cur->in[cur->ninputs++].from = from;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
/* Renderer fixtures have no guest CPU; exercise the neutral unavailable projection.
|
||||
* Atomic publication is covered independently by postfx_publication_test. */
|
||||
#include "gte_postfx_projection.h"
|
||||
void gte_postfx_projection_enable(int enabled) {(void)enabled;}
|
||||
int gte_postfx_projection_read(GtePostfxProjection *out) {(void)out;return 0;}
|
||||
@@ -11,10 +11,17 @@
|
||||
* bottom-up) so a run against another tree can be compared byte for byte
|
||||
* (BLOOM_FIXTURE_NO_BLOOM builds it without the bloom API). */
|
||||
#define GL_PRESENT_TEST_HOOK(gen) bloom_capture_hook(gen)
|
||||
#include <stdint.h>
|
||||
static uint64_t pfx_fixture_clock(void);
|
||||
#define PFX_NOW_TICKS() pfx_fixture_clock()
|
||||
static void bloom_capture_hook(int gen);
|
||||
#define main scale_fixture_main
|
||||
#include "test_gl_scale_invariance.c"
|
||||
#undef main
|
||||
static uint64_t s_pfx_test_clock;
|
||||
static uint64_t pfx_fixture_clock(void) {
|
||||
return s_pfx_test_clock ? s_pfx_test_clock : SDL_GetPerformanceCounter();
|
||||
}
|
||||
void gpu_hd_textures_begin_upload(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
|
||||
void gpu_hd_textures_track_upload(int x,int y,int w,int h,const uint16_t* p){(void)x;(void)y;(void)w;(void)h;(void)p;}
|
||||
void gpu_hd_textures_invalidate(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
|
||||
@@ -54,6 +61,7 @@ static void bloom_capture_hook(int gen) {
|
||||
}
|
||||
|
||||
enum { SC_2D, SC_ADD, SC_3D, SC_HUD_OVER_3D, SC_PARTIAL, SC_N };
|
||||
enum { SC_DEPTH_ONLY = 99 };
|
||||
static const char *k_scene[SC_N] = { "2d", "additive", "3d", "hud-over-3d", "partial" };
|
||||
/* SC_PARTIAL's grey HUD panels (native x, y, w, h): one beside the lamp, one
|
||||
* covering part of it. */
|
||||
@@ -77,8 +85,10 @@ static uint8_t *frame(int scene, float bloom, uint64_t *vram_hash) {
|
||||
glb_set_mask_bits(0, 0); glb_set_semi_transparency(0, 0);
|
||||
glb_fill_rect(0, 0, FRAME_W, FRAME_H, 0x0000);
|
||||
/* A dark 3D ground everywhere: lit (may take a halo), below the threshold. */
|
||||
tri3d(0, 0, FRAME_W, 0, 0, FRAME_H, 0x0842);
|
||||
tri3d(FRAME_W, 0, FRAME_W, FRAME_H, 0, FRAME_H, 0x0842);
|
||||
if (scene != SC_DEPTH_ONLY) {
|
||||
tri3d(0, 0, FRAME_W, 0, 0, FRAME_H, 0x0842);
|
||||
tri3d(FRAME_W, 0, FRAME_W, FRAME_H, 0, FRAME_H, 0x0842);
|
||||
}
|
||||
switch (scene) {
|
||||
case SC_2D: /* a white HUD panel */
|
||||
glb_draw_flat_rect(120, 90, 80, 60, 0x7fff);
|
||||
@@ -88,6 +98,7 @@ static uint8_t *frame(int scene, float bloom, uint64_t *vram_hash) {
|
||||
glb_draw_flat_triangle(120, 150, 200, 150, 160, 80, 0x7fff);
|
||||
glb_set_semi_transparency(0, 0);
|
||||
break;
|
||||
case SC_DEPTH_ONLY:
|
||||
case SC_3D: /* a lit 3D polygon */
|
||||
tri3d(120, 150, 200, 150, 160, 80, 0x7fff);
|
||||
break;
|
||||
@@ -234,6 +245,7 @@ int main(int argc, char **argv) {
|
||||
fputs("void main(){ o = this is not glsl; }\n", f);
|
||||
fclose(f);
|
||||
}
|
||||
gl_renderer_render_thread_stop();
|
||||
for (int pass = 0; pass < 2; pass++) { /* 0: this thread, 1: render thread */
|
||||
uint64_t h = 0;
|
||||
gl_renderer_set_post_shader("pfx_invert.pfx");
|
||||
@@ -261,6 +273,69 @@ int main(int argc, char **argv) {
|
||||
check(lit > s_cap_w * s_cap_h / 2, "mask input: the 3D ground and lamp are lit");
|
||||
free(m);
|
||||
|
||||
/* A history input also works on the final window pass. The blue
|
||||
* channel observes the shader clock, so reload must reset its origin. */
|
||||
FILE *history_file=fopen("pfx_history.glsl","w");
|
||||
check(history_file!=NULL,"open history shader");
|
||||
if(history_file) {
|
||||
fputs("void main(){ float r=texelFetch(u_prev,ivec2(gl_FragCoord.xy)-u_org,0).r;"
|
||||
"o=vec4(r*0.5+u_add,0.0,clamp(u_time/4.0,0.0,1.0),1.0); }\n",history_file);
|
||||
fclose(history_file);
|
||||
}
|
||||
history_file=fopen("pfx_history.pfx","w");
|
||||
if(history_file) {
|
||||
fputs("pfx 1\npass history\n shader pfx_history.glsl\n input u_prev history\n uniform u_add 0.25\n",history_file);
|
||||
fclose(history_file);
|
||||
}
|
||||
psx_host_set_post_uniform("u_add",0.25f,0,0,0);
|
||||
s_pfx_test_clock=100*SDL_GetPerformanceFrequency();
|
||||
gl_renderer_set_post_shader("pfx_history.pfx");
|
||||
uint8_t *hist=frame(SC_PARTIAL,0,&h);
|
||||
size_t center=((size_t)(s_cap_h/2)*s_cap_w+s_cap_w/2)*3;
|
||||
check(abs(hist[center]-64)<=1 && hist[center+2]<=1,"first history frame is cleared and starts at time zero");
|
||||
free(hist);
|
||||
s_pfx_test_clock=102*SDL_GetPerformanceFrequency();
|
||||
hist=frame(SC_PARTIAL,0,&h);
|
||||
check(abs(hist[center]-96)<=1 && abs(hist[center+2]-128)<=1,"final pass samples the previous real image and its own elapsed time");
|
||||
free(hist);
|
||||
if(!s_rth_on) {
|
||||
for(int repeat=0;repeat<2;++repeat) {
|
||||
postfx_present(0,0,0,0,s_cap_w,s_cap_h);
|
||||
bloom_capture_hook(1);
|
||||
check(abs(s_cap[center]-112)<=1,"generated presents do not advance history");
|
||||
}
|
||||
}
|
||||
psx_host_set_post_uniform("u_add",0.125f,0,0,0);
|
||||
hist=frame(SC_PARTIAL,0,&h);
|
||||
check(abs(hist[center]-80)<=1,"live uniform update reaches the actual shader");
|
||||
free(hist);
|
||||
s_pfx_test_clock=104*SDL_GetPerformanceFrequency();
|
||||
gl_renderer_set_post_shader("pfx_history.pfx");
|
||||
hist=frame(SC_PARTIAL,0,&h);
|
||||
check(abs(hist[center]-32)<=1 && hist[center+2]<=1,"reload clears history and resets shader time while retaining live values");
|
||||
free(hist);
|
||||
s_pfx_test_clock=0;
|
||||
|
||||
FILE *depth_file=fopen("pfx_depth.glsl","w");
|
||||
if(depth_file) {
|
||||
fputs("void main(){ float d=texelFetch(u_depth,ivec2(gl_FragCoord.xy)-u_org,0).r;"
|
||||
"o=vec4(clamp(d/4000.0,0.0,1.0),0.0,0.0,1.0); }\n",depth_file);
|
||||
fclose(depth_file);
|
||||
}
|
||||
depth_file=fopen("pfx_depth.pfx","w");
|
||||
if(depth_file) {
|
||||
fputs("pfx 1\npass depth\n shader pfx_depth.glsl\n input u_depth depth\n",depth_file);
|
||||
fclose(depth_file);
|
||||
}
|
||||
gl_renderer_set_pgxp_depth(1);
|
||||
gl_renderer_set_post_shader("pfx_depth.pfx");
|
||||
uint8_t *depth=frame(SC_DEPTH_ONLY,0,&h);
|
||||
size_t outside=((size_t)(s_cap_h-1-20)*s_cap_w+20)*3;
|
||||
check(abs(depth[center]-128)<=3,"depth input reconstructs the triangle's GTE screen depth");
|
||||
check(depth[outside]<=1,"depth input is zero where no 3D primitive drew");
|
||||
free(depth);
|
||||
gl_renderer_set_pgxp_depth(0);
|
||||
|
||||
gl_renderer_set_post_shader("pfx_screen.pfx");
|
||||
check(gl_renderer_post_shader_loaded(), "source_down + blend screen chain loads");
|
||||
uint8_t *sc = frame(SC_PARTIAL, 0.0f, &h);
|
||||
|
||||
@@ -34,6 +34,7 @@ static void fixture_present(int generated);
|
||||
static void fixture_window(int slot);
|
||||
#define GL_PRESENT_THREAD_TEST_HOOK(slot) fixture_window(slot)
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gl_postfx_projection_fixture.h"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
#include "gl_batch_policy_lle.c"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Original source-owned GL readback-coherence regression. No retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gl_postfx_projection_fixture.h"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
#include "gl_batch_policy_lle.c"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
* surface), and with the thread on its record/acquire counters.
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gl_postfx_projection_fixture.h"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
#include "gl_batch_policy_lle.c"
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
* 3x as the nearest rescale of all their pixels, before any redraw.
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gl_postfx_projection_fixture.h"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
#include "gl_batch_policy_lle.c"
|
||||
@@ -102,6 +103,15 @@ int psx_netplay_active(void){return 0;}
|
||||
GrBackend gr_backend(void){return GR_BACKEND_OPENGL;}
|
||||
void gr_refresh_backend(void){}
|
||||
#endif
|
||||
/* These renderer-only fixtures have no frontend telemetry/menu consumer. */
|
||||
void psx_video_telemetry_compose(int r,int s,int sw,int sh,int dw,int dh,int vw,int vh) {
|
||||
(void)r;(void)s;(void)sw;(void)sh;(void)dw;(void)dh;(void)vw;(void)vh;
|
||||
}
|
||||
void psx_video_telemetry_present(int result) {(void)result;}
|
||||
int psx_host_menu_overlay_image(const uint32_t **p,int *w,int *h) {
|
||||
*p=NULL;*w=*h=0;return 0;
|
||||
}
|
||||
|
||||
int gpu_display_is_depth24(void){return 0;}
|
||||
void gpu_get_display_info(GpuDisplayInfo *out){memset(out,0,sizeof(*out));out->width=320;out->height=240;}
|
||||
int psx_ws_prim_in_backdrop(void){return 0;}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#include "gte_projection_publication.hpp"
|
||||
#include "postfx_debug_json.h"
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <thread>
|
||||
static int failures;
|
||||
static void check(bool ok,const char* why) {
|
||||
if(!ok){std::fprintf(stderr,"%s\n",why);++failures;}
|
||||
}
|
||||
int main() {
|
||||
GteProjectionPublication state; GtePostfxProjection out{};
|
||||
state.publish(1,2,3); check(!state.read(out),"disabled projection is unavailable");
|
||||
state.enable(true); state.publish(1,2,3);
|
||||
check(state.read(out)&&out.h==1&&out.ofx==2&&out.ofy==3,"first enabled tuple");
|
||||
state.enable(false);state.enable(true);
|
||||
check(!state.read(out),"re-enabled session cannot reuse a stale tuple");
|
||||
state.publish(1,2,3);check(state.read(out),"unchanged values publish in the new epoch");
|
||||
std::atomic<bool> done{false},bad{false};
|
||||
std::thread writer([&]{for(int i=1;i<200000;++i)state.publish(i,i*2,i*3);done=true;});
|
||||
auto read=[&]{while(!done.load()){GtePostfxProjection p{};
|
||||
if(state.read(p)&&(p.ofx!=p.h*2||p.ofy!=p.h*3))bad=true;}};
|
||||
std::thread a(read),b(read);writer.join();a.join();b.join();
|
||||
check(!bad,"concurrent readers never see mixed projection fields");
|
||||
PfxDebugLive live{};std::strcpy(live.name,"u_\"x\n");live.v[0]=1.25f;
|
||||
char json[512];check(pfx_debug_json(json,sizeof json,"C:\\a\"b\n","bad\tpreset",&live,1)>0,"serialize escaped diagnostics");
|
||||
check(std::strstr(json,R"("path":"C:\\a\"b\u000a")")!=nullptr,"path round trips without mutation");
|
||||
check(std::strstr(json,R"("u_\"x\u000a":[1.25,0,0,0])")!=nullptr,"uniform names are escaped");
|
||||
std::printf("{%s}\n",json);
|
||||
for(int cap=0;cap<160;++cap) {
|
||||
char guarded[192];std::memset(guarded,'X',sizeof guarded);
|
||||
const int n=pfx_debug_json(guarded+1,cap,"C:\\a\"b\n","bad\tpreset",&live,1);
|
||||
check(guarded[0]=='X'&&guarded[cap+1]=='X',"small output buffer stays within bounds");
|
||||
if(!n&&cap)check(guarded[1]==0,"failed JSON output is empty");
|
||||
}
|
||||
live.v[0]=std::numeric_limits<float>::infinity();
|
||||
check(!pfx_debug_json(json,sizeof json,"","",&live,1)&&!json[0],"nonfinite values fail without invalid JSON");
|
||||
return failures?1:0;
|
||||
}
|
||||
Reference in New Issue
Block a user