Skip to content

OpenGL GPU Downscaled Readback

github-actions[bot] edited this page Sep 15, 2026 · 1 revision

OpenGL GPU-Downscaled Readback

Lifecycle: Performance - Unproven · Type: Transfer-reduction and conversion optimization
Reviewed source: 322986f4c5502b3fa887a9605ac8cd88e9b2c579
State: The oversized readback and CPU shrink loop are source-confirmed. No OpenGL benchmark has measured the savings.

OpenGL candidate directory · OpenGL Renderer · GPU-resident surface consumers

Purpose

When an internal surface is rendered at a scale greater than one and later downloaded to guest memory, the OpenGL path reads back the scaled image into a CPU buffer and then selects one source texel per guest texel in a CPU loop.

At a scale factor of four in both dimensions, the host reads sixteen times the guest-resolution pixel count before discarding fifteen of every sixteen spatial samples. The candidate performs the exact shrink selection on the GPU first and reads back only the guest-resolution representation.

This is not a generic filtered resize proposal. The first accepted subset must reproduce the current integer sample-selection rule exactly.

Source seams

  • hw/xbox/nv2a/pgraph/gl/surface.c
    • surface_download_to_buffer()
    • surface_copy_shrink_row()
    • surface format, pitch, swizzle, flip, and scaling logic
  • Surface creation and scratch resources in the OpenGL renderer state.
  • Guest RAM writeback and dirty/freshness ownership after the download.

Current source allocates scale_factor² * surface->size, reads the scaled framebuffer into it, then runs a CPU row loop marked with FIXME: Replace this with a hw accelerated version.

Current execution flow

flowchart TD
    A[Scaled GPU surface] --> B[Read scaled width x scaled height]
    B --> C[Store expanded pixels in CPU scale buffer]
    C --> D[For each guest row]
    D --> E[Select every scale-th source pixel]
    E --> F[Optional CPU swizzle/flip]
    F --> G[Write guest-resolution bytes to guest RAM]
Loading

Approximate transfer multiplier:

readback pixels / guest pixels = scale_factor²

2x scale -> 4x pixels
3x scale -> 9x pixels
4x scale -> 16x pixels

Pitch and format details determine exact byte counts, but the area multiplier remains the core issue.

Candidate execution flow

flowchart TD
    A[Scaled GPU surface] --> B{Format/subset supported?}
    B -->|No| C[Current full readback + CPU shrink]
    B -->|Yes| D[Render/copy exact selected texels into guest-size scratch]
    D --> E[Read back guest-size scratch only]
    E --> F[Optional CPU swizzle/flip if still required]
    F --> G[Write guest RAM]
Loading

An exact nearest-sample shader can be shaped as:

#version 330
uniform sampler2D scaled_surface;
uniform ivec2 guest_size;
uniform int scale_factor;
layout(location = 0) out vec4 out_color;

void main()
{
    ivec2 dst = ivec2(gl_FragCoord.xy);
    ivec2 src = dst * scale_factor;
    out_color = texelFetch(scaled_surface, src, 0);
}

The source origin, Y direction, row pitch, and format interpretation must be matched against surface_copy_shrink_row() and the current readback function. The snippet is only the basic sample-selection model.

Exact route options

Render into a guest-size scratch texture

  • Attach a guest-resolution texture to an FBO.
  • Sample exact source texels with texelFetch().
  • Read only the scratch texture.

This supports custom conversion but introduces a draw and temporary attachment lifetime.

Blit/copy subset

A nearest blit may be usable only if its source/destination coordinate rules select exactly the same texels as the current CPU algorithm and format conversion does not change bytes.

Compute/image path

OpenGL compute or image load/store could write packed guest bytes into a buffer. This may help depth/stencil or packed formats, but it is a separate complexity tier and requires stronger capability checks.

What may disappear

  • scale_factor² oversized readback traffic.
  • Large pg->scale_buf allocation and memory bandwidth.
  • CPU row/texel selection loop.
  • Some CPU cache pollution and copy latency.
  • Potentially part of later format conversion if moved into the GPU pass.

What remains mandatory

  • Exact selected source texel for the initial admitted subset.
  • Guest pitch, byte order, alpha semantics, and row orientation.
  • Swizzle behavior and partial-surface handling.
  • Depth/stencil packing semantics.
  • Completion before the guest CPU or another device reads the bytes.
  • Reference path for unsupported formats or conversion rules.
  • Correct surface freshness and dirty-bit transitions.

Why a generic filtered downscale is unsafe

Linear or implementation-defined filtering can blend adjacent host texels. Guest RAM expects one exact emulated texel representation, not a visually similar image.

For coordinate-coded input:

source pixel (x, y) contains unique encoded coordinates

The expected output can prove whether the GPU route selected the same sample as the CPU reference for each destination coordinate.

Depth and stencil

Depth/stencil readback should not be included in the first color prototype. Writing depth through a normalized color attachment can alter representation. Packed D24S8, floating depth, scale conversion, and separate stencil extraction may need:

  • integer texture views;
  • compute packing;
  • separate aspect reads;
  • or continued reference CPU handling.

Each format family should be admitted independently.

PBO relationship

A pixel-buffer-object ring can overlap eligible readbacks with later CPU work. It does not reduce bytes and cannot make an immediate guest CPU dependency asynchronous.

The two experiments answer different questions:

GPU downscale: how many bytes must cross to the CPU?
PBO ring: when can the CPU wait/map those bytes?

Do not combine them in the first benchmark.

Advantages

  • Removes a clear scale_factor² amplification.
  • Particularly relevant at common 2x–4x internal scaling.
  • Has an exact coordinate-coded correctness oracle.
  • Can be introduced format by format.
  • Keeps current readback as a conservative fallback.

Costs and risks

  • Adds GPU pass and scratch-resource management.
  • The GPU may already be saturated, making CPU shrink cheaper on some systems.
  • Incorrect origin, pitch, or sample selection silently corrupts guest RAM.
  • Extra FBO/state changes can disturb the main renderer without a proper state shadow.
  • Depth/stencil and swizzled formats require separate logic.
  • If readbacks are rare, the path may not matter despite a large per-call saving.

Required counters

surface_readbacks
surface_readbacks_scaled
scaled_readback_source_bytes
guest_resolution_readback_bytes
CPU_shrink_rows
CPU_shrink_us
GPU_downscale_passes
GPU_downscale_us
fallbacks{format_or_reason}
wait_us_for_guest_visibility
surface_readback_p95_us

Useful derived values:

bytes avoided = scaled source bytes - guest-size bytes
route hit rate = GPU downscale passes / eligible scaled readbacks

Qualification matrix

Scenario Required result
Coordinate-coded RGBA image at 1x/2x/3x/4x Byte-identical guest output
Nontrivial pitch Correct row placement and no overrun
Flipped readback Same orientation as reference
Partial surface/range Only intended bytes updated
Swizzled destination Same post-swizzle bytes
Multiple supported color formats Format-specific exact comparison
Unsupported depth/stencil Clean reference fallback
Immediate guest CPU read Correct completion/wait
Snapshot/scale change/resource recreation Scratch invalidated/recreated safely
Release workload with frequent scaled readback Lower bytes/CPU/tail time

Acceptance rule

Promote to Researched after byte-exact fixtures pass for a declared format subset, route counters show meaningful scaled-readback frequency, and Release measurements reduce transferred bytes and total readback latency without creating a larger GPU bottleneck.

Rejection rule

Reject or narrow the route if exact source sampling cannot be established, if the GPU pass costs more than the CPU shrink in representative workloads, or if scaled readback is not reached often enough to affect performance.

Related

Clone this wiki locally