-
Notifications
You must be signed in to change notification settings - Fork 0
Cropping and geometry
src/render/video_renderer.cpp (AnalyzeContentBounds), src/render/shaders.h
(pass 2)
Crop, line doubling and rotation all happen in pass 2, before scaling. That
ordering is what makes them appear in recordings and screenshots as well as in
the window — outputWidth() / outputHeight() report the size after all
three, and that is the size everything outside the shader works with.
Dragged on the picture itself. The four edges are stored per profile in source
pixels, and the overlay converts between window coordinates and source
coordinates through videoRect().
Known issue: when a crop edge is dragged over the overlay's own Apply and Cancel controls, those controls become too dark to read. Not yet fixed.
AnalyzeContentBounds() runs continuously on the CPU over the raw capture
buffer, and Detect applies what it has measured — unless too much of the picture
would go, which is its own section below.
static const int kContentLuma = 24;Anything above that is picture rather than border. Analogue black does not arrive at zero and it is not quiet, so the bar sits a little above where the blacker-than-black of a limited range signal would be.
const int minRun = max(w / (50 * kBoundsColumnStep), 2);A row counts as picture only when a decent stretch of it — about 2 % of the columns sampled — is above black. One bright speck of analogue noise in the letterbox must not widen the crop.
Rows are scanned every second line, columns every second pixel
(kBoundsColumnStep = 2). Each lit pixel also votes for its column in
columnHits_.
const int minCol = max(litRows / 20, 1);A column has to be lit in at least 5 % of the rows that carry picture at all. Same idea the other way round.
An entirely black frame returns early and says nothing.
if (left < accL_) accL_ = left; // and so on for all four edgesThe result is the union across five sampled frames, because a fade to black
is not evidence that the picture got smaller. Only after
kBoundsFramesWanted = 5 windows is the accumulated rectangle published, and
the accumulator resets.
The bounds are logged only when they move, not every window — the border is a fixed property of the signal, so a line that keeps reappearing in the log means something is wrong.
kBoundsSampleEvery = 9 — every ninth frame, and every second column.
This is the only one of the three analyses that never finishes. The range and interlace verdicts latch and stop; this one keeps running for the whole session, so it is the only one still costing anything once the picture has settled.
And it was costing enough to matter. A full-width scan of every third frame lands ten times a second, and measured against the deinterlacer, ten per cent of its field switches were arriving more than ten milliseconds late. That is a stutter you can see. The border is in no hurry, so the same answer arrives just as reliably from a sixth of the work.
RGB layouts arrive bottom-up. The scan reads buffer order, so the two vertical edges are swapped on the way out:
if (source_.bottomUp) { const int t = h - 1 - bottom; bottom = h - 1 - top; top = t; }Both scans step, so each far edge can fall one step short and is nudged outward by the step size before being stored.
The measurement finds the edges of what is not black. It cannot tell a border from a part of the picture that simply is not lit at this moment — and a console sitting on its home screen or a splash logo is exactly that case. Cropping there throws away picture area and keeps the logo.
So DetectCrop() asks how much would survive, and the statistic is area:
if (partW * partH < 0.5 || partW < 0.4 || partH < 0.4) { /* refuse, and say why */ }A real border eats one direction. A logo on black eats both. Measured, on a 720×480 signal:
| Width | Height | Area | ||
|---|---|---|---|---|
| GameCube home screen | 48 % | 77 % | 37 % | refused |
| 2.35:1 in 4:3 — the widest border that is still a border | 100 % | 57 % | 57 % | applied |
| 4:3 in 16:9 | 75 % | 100 % | 75 % | applied |
| PAL overscan | 93 % | 93 % | 87 % | applied |
Over the edges alone that would be five points of margin; over the area it is twenty. The second condition is only against the degenerate case — a narrow strip can hold half the area and still be no border.
The refusal names the number it refused on, so it can be checked rather than believed. Six consecutive live measurements of the GameCube home screen came out between 35 % and 37 %; that they moved at all is its own evidence, since a border does not wander and a rotating cube does.
For 240p and 288p sources that arrive half as tall as they should. gLineDouble
in the pass-2 constant buffer; the logical picture becomes twice as tall and the
row index is ly >> 1.
This is a different thing from the co-sited field case in Deinterlacing: line doubling is for a source that genuinely arrives at half height, where co-siting is for a half-height source the card has already packed into a full-height interlaced frame.
Rotation in src/config.h: None, Cw90, Half, Ccw90.
Rotation is undone first in pass 2, before anything else:
if (gRotation == 1) { lx = op.y; ly = logicalH - 1 - op.x; }
else if (gRotation == 2) { lx = logicalW - 1 - op.x; ly = logicalH - 1 - op.y; }
else if (gRotation == 3) { lx = logicalW - 1 - op.y; ly = op.x; }
else { lx = op.x; ly = op.y; }Everything below that line works in the picture's own orientation, which is the only one in which "the line above" means anything — a deinterlacer running in rotated coordinates would be reconstructing across columns.
Note that logicalH accounts for line doubling but a quarter turn swaps the axes
of the render target only, not of the logical picture.