mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
4b4fddb769
A full-screen rect backdrop that stops short of the reveal vetoes polygon overhang (Tomba village, beads-eio.4.28). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
193 lines
8.3 KiB
C
193 lines
8.3 KiB
C
#ifndef PSX_WS_BACKDROP_EXTENT_H
|
|
#define PSX_WS_BACKDROP_EXTENT_H
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
/* Finite 2D backdrop extent (native-wide scene classifier input).
|
|
*
|
|
* Pre-rendered 2D scenes draw their whole background as a few large textured
|
|
* RECTANGLES at the back of the frame, before any polygon. Rect primitives are
|
|
* screen-space blits by definition: nothing projects them, so the authored
|
|
* image is exactly as wide as those rects and no wider. When that image covers
|
|
* the canonical display but stops short of the native-wide reveal, the reveal
|
|
* can only show void (black) with world props floating over it. Polygon
|
|
* overhang cannot tell that apart from a real 3D world: widened 2D actor culls
|
|
* legitimately place sprite quads past the canonical edge (Tomba's overhead
|
|
* village: a 384x256 ground image, fence/sign quads out to x=428).
|
|
*
|
|
* Per frame: the leading run of draw primitives that are rectangles (closed by
|
|
* the first polygon or line) contributes its untagged, unstretched textured
|
|
* rects to a coarse coverage grid spanning [-reveal, W + reveal). A frame whose
|
|
* run covers >= 90% of the canonical display is a full-screen backdrop frame;
|
|
* if any reveal cell in the rows that backdrop occupies is uncovered, the
|
|
* frame is SHORT. Two short frames within the sustain gap arm a veto that
|
|
* presents the scene at native 4:3 until short frames stop arriving.
|
|
* Host-derived presentation evidence: reset on load, never serialized. */
|
|
|
|
#define WS_BDX_CELL 8 /* column width, drawing-space pixels */
|
|
#define WS_BDX_COLS 1024 /* analysable span: 8192 px */
|
|
#define WS_BDX_ROWS 32 /* row bands across the display height */
|
|
#define WS_BDX_FULL_PCT 90 /* canonical coverage that makes a backdrop */
|
|
#define WS_BDX_SUSTAIN 6u /* max frames between two short verdicts */
|
|
|
|
typedef struct {
|
|
/* accumulating frame */
|
|
uint32_t frame;
|
|
int started, open;
|
|
int32_t W, H, R, x_lo, ncols, row_h, nrows;
|
|
int32_t min_x, max_x; /* union x-extent of candidates */
|
|
uint32_t rects;
|
|
uint32_t mask[WS_BDX_COLS]; /* bit r = row band r covered */
|
|
/* newest verdict (observability) */
|
|
uint32_t eval_frame, eval_rects;
|
|
int32_t eval_min_x, eval_max_x, eval_reveal;
|
|
int eval_full, eval_short;
|
|
uint32_t canon_pct, left_pct, right_pct;
|
|
/* veto stamps */
|
|
int has_short, has_sust;
|
|
uint32_t last_short, sust_short;
|
|
uint64_t evaluations, short_frames;
|
|
} WsBackdropExtent;
|
|
|
|
static inline void ws_bdx_reset(WsBackdropExtent* s) { memset(s, 0, sizeof *s); }
|
|
|
|
static inline int32_t ws_bdx_floordiv(int32_t a, int32_t b) {
|
|
return a >= 0 ? a / b : -((-a + b - 1) / b);
|
|
}
|
|
|
|
/* Smallest cell index whose centre is >= x. */
|
|
static inline int32_t ws_bdx_col_at(const WsBackdropExtent* s, int32_t x) {
|
|
return ws_bdx_floordiv(x - s->x_lo - WS_BDX_CELL / 2 + WS_BDX_CELL - 1,
|
|
WS_BDX_CELL);
|
|
}
|
|
|
|
static inline int ws_bdx_popcount(uint32_t v) {
|
|
int n = 0;
|
|
while (v) { v &= v - 1u; ++n; }
|
|
return n;
|
|
}
|
|
|
|
static inline void ws_bdx_evaluate(WsBackdropExtent* s) {
|
|
if (!s->started || !s->rects || s->R <= 0 || s->W <= 0 || s->H <= 0) return;
|
|
int32_t c0 = ws_bdx_col_at(s, 0), c1 = ws_bdx_col_at(s, s->W);
|
|
if (c0 < 0) c0 = 0;
|
|
if (c1 > s->ncols) c1 = s->ncols;
|
|
uint32_t all_rows = s->nrows >= 32 ? 0xFFFFFFFFu : ((1u << s->nrows) - 1u);
|
|
uint32_t canon_cells = 0, canon_cov = 0;
|
|
uint32_t row_hits[WS_BDX_ROWS] = {0};
|
|
for (int32_t c = c0; c < c1; ++c) {
|
|
uint32_t m = s->mask[c] & all_rows;
|
|
canon_cells += (uint32_t)s->nrows;
|
|
canon_cov += (uint32_t)ws_bdx_popcount(m);
|
|
for (int r = 0; r < s->nrows; ++r) if (m & (1u << r)) ++row_hits[r];
|
|
}
|
|
s->evaluations++;
|
|
s->eval_frame = s->frame;
|
|
s->eval_rects = s->rects;
|
|
s->eval_min_x = s->min_x; s->eval_max_x = s->max_x;
|
|
s->eval_reveal = s->R;
|
|
s->canon_pct = canon_cells ? canon_cov * 100u / canon_cells : 0;
|
|
s->eval_full = canon_cells && canon_cov * 100u >= canon_cells * WS_BDX_FULL_PCT;
|
|
s->eval_short = 0;
|
|
s->left_pct = s->right_pct = 0;
|
|
if (!s->eval_full) return;
|
|
/* Rows the backdrop actually occupies: a letterboxed image does not demand
|
|
* coverage in bands it leaves empty inside the canonical frame too. */
|
|
uint32_t need = 0, span = (uint32_t)(c1 - c0);
|
|
for (int r = 0; r < s->nrows; ++r)
|
|
if (row_hits[r] * 100u >= span * WS_BDX_FULL_PCT) need |= 1u << r;
|
|
uint32_t lc = 0, ln = 0, rc = 0, rn = 0;
|
|
int nrow = ws_bdx_popcount(need);
|
|
for (int32_t c = 0; c < s->ncols; ++c) {
|
|
if (c >= c0 && c < c1) continue;
|
|
uint32_t got = (uint32_t)ws_bdx_popcount(s->mask[c] & need);
|
|
if (c < c0) { lc += got; ln += (uint32_t)nrow; }
|
|
else { rc += got; rn += (uint32_t)nrow; }
|
|
}
|
|
/* Reveal wider than the analysable grid: the union extent must at least
|
|
* reach the true reveal edges. */
|
|
int32_t grid_lo = s->x_lo, grid_hi = s->x_lo + s->ncols * WS_BDX_CELL;
|
|
int beyond_short = (grid_lo > -s->R && s->min_x > -s->R) ||
|
|
(grid_hi < s->W + s->R && s->max_x < s->W + s->R);
|
|
s->left_pct = ln ? lc * 100u / ln : 100u;
|
|
s->right_pct = rn ? rc * 100u / rn : 100u;
|
|
s->eval_short = lc < ln || rc < rn || beyond_short;
|
|
if (!s->eval_short) return;
|
|
s->short_frames++;
|
|
if (s->has_short && s->frame - s->last_short <= WS_BDX_SUSTAIN) {
|
|
s->sust_short = s->frame;
|
|
s->has_sust = 1;
|
|
}
|
|
s->last_short = s->frame;
|
|
s->has_short = 1;
|
|
}
|
|
|
|
/* Start (or continue) accumulating `frame`. W/H = canonical display size,
|
|
* R = configured per-side native-wide reveal. A new frame finalizes the
|
|
* previous one if its leading run never closed. */
|
|
static inline void ws_bdx_frame(WsBackdropExtent* s, uint32_t frame,
|
|
int32_t W, int32_t H, int32_t R) {
|
|
if (s->started && s->frame == frame) return;
|
|
if (s->started && (int32_t)(frame - s->frame) < 0) {
|
|
ws_bdx_reset(s); /* frame rewind: forget verdicts */
|
|
} else if (s->started && s->open) {
|
|
ws_bdx_evaluate(s);
|
|
}
|
|
s->started = 1; s->open = 1; s->frame = frame;
|
|
s->W = W; s->H = H; s->R = R < 0 ? 0 : R;
|
|
int32_t span = W + 2 * s->R;
|
|
int32_t ncols = (span + WS_BDX_CELL - 1) / WS_BDX_CELL;
|
|
if (ncols > WS_BDX_COLS) { /* clamp symmetrically */
|
|
ncols = WS_BDX_COLS;
|
|
s->x_lo = (W - ncols * WS_BDX_CELL) / 2;
|
|
} else {
|
|
s->x_lo = -s->R;
|
|
}
|
|
s->ncols = ncols < 0 ? 0 : ncols;
|
|
s->row_h = H > 0 ? (H + WS_BDX_ROWS - 1) / WS_BDX_ROWS : 1;
|
|
s->nrows = H > 0 ? (H + s->row_h - 1) / s->row_h : 0;
|
|
s->rects = 0;
|
|
s->min_x = INT32_MAX; s->max_x = INT32_MIN;
|
|
memset(s->mask, 0, sizeof s->mask);
|
|
}
|
|
|
|
/* A backdrop candidate inside the leading run: [x0,x1) x [y0,y1). */
|
|
static inline void ws_bdx_rect(WsBackdropExtent* s, int32_t x0, int32_t y0,
|
|
int32_t x1, int32_t y1) {
|
|
if (!s->started || !s->open || x1 <= x0 || y1 <= y0) return;
|
|
s->rects++;
|
|
if (x0 < s->min_x) s->min_x = x0;
|
|
if (x1 > s->max_x) s->max_x = x1;
|
|
int32_t ca = ws_bdx_col_at(s, x0), cb = ws_bdx_col_at(s, x1);
|
|
if (ca < 0) ca = 0;
|
|
if (cb > s->ncols) cb = s->ncols;
|
|
/* Row band r is covered when its centre lies inside [y0, y1). */
|
|
int32_t ra = ws_bdx_floordiv(y0 - s->row_h / 2 + s->row_h - 1, s->row_h);
|
|
int32_t rb = ws_bdx_floordiv(y1 - s->row_h / 2 + s->row_h - 1, s->row_h);
|
|
if (ra < 0) ra = 0;
|
|
if (rb > s->nrows) rb = s->nrows;
|
|
if (ca >= cb || ra >= rb) return;
|
|
uint32_t bits = 0;
|
|
for (int32_t r = ra; r < rb; ++r) bits |= 1u << r;
|
|
for (int32_t c = ca; c < cb; ++c) s->mask[c] |= bits;
|
|
}
|
|
|
|
/* The first polygon or line ends the backdrop run for this frame. */
|
|
static inline void ws_bdx_close(WsBackdropExtent* s) {
|
|
if (!s->started || !s->open) return;
|
|
s->open = 0;
|
|
ws_bdx_evaluate(s);
|
|
}
|
|
|
|
/* Veto native-wide: a sustained short full-screen backdrop, still recent. */
|
|
static inline int ws_bdx_veto(const WsBackdropExtent* s, uint32_t frame,
|
|
uint32_t grace) {
|
|
if (!s->has_sust) return 0;
|
|
/* The newest short frame must itself belong to a sustained pair; an
|
|
* isolated short frame after a quiet spell does not re-arm the veto. */
|
|
if (s->sust_short != s->last_short) return 0;
|
|
if ((int32_t)(frame - s->sust_short) < 0) return 0;
|
|
return frame - s->sust_short <= grace;
|
|
}
|
|
#endif
|