Skip to content

OpenGL Native Depth Variant

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

OpenGL Native Depth Variant

Lifecycle: Performance - Unproven · Type: Numerical and early-depth experiment
Reviewed source: 322986f4c5502b3fa887a9605ac8cd88e9b2c579
State: Source-backed. No OpenGL GPU benchmark or compatibility campaign has established a useful admitted subset.

OpenGL candidate directory · OpenGL Renderer · Shared shader architecture

Purpose

The shared fragment generator writes gl_FragDepth for every generated normal fragment shader. Integer depth formats explicitly floor guest depth values and compensate for host depth-conversion differences. This is deliberate emulation logic, not an accidental assignment.

The proposed experiment creates a genuinely different shader/pipeline variant that relies on rasterizer-produced host depth for a narrow state subset. The main possible benefit is not the removal of a few arithmetic instructions. Avoiding a fragment-depth write may allow the driver to reject occluded fragments before executing the full texture and combiner program.

Source seams

  • hw/xbox/nv2a/pgraph/glsl/psh.c
    • pgraph_glsl_set_psh_state()
    • pgraph_glsl_gen_psh()
    • depth-format-specific gl_FragDepth emission
  • Shared vertex/geometry shader generation and the OpenGL program key must include the selected depth policy.
  • OpenGL depth/raster state is applied from gl/draw.c.

Current source emits logic equivalent to:

// D16
float zvalue = ...;
gl_FragDepth = floor(zvalue) / 65535.0;

// D24
uint bits = floatBitsToUint(floor(zvalue) / 16777216.0) + 1u;
gl_FragDepth = uintBitsToFloat(bits);

The D24 expression intentionally compensates for how different host GPUs quantize floating-point fragment depth into integer depth attachments.

Current execution flow

flowchart TD
    A[Vertex/geometry shader produces position] --> B[Rasterizer interpolates host depth]
    B --> C[Fragment shader executes texture and combiner work]
    C --> D[Compute guest-oriented zvalue]
    D --> E[Floor/encode depth for D16 or D24]
    E --> F[Write gl_FragDepth]
    F --> G[Host depth/stencil result]
Loading

A fragment-depth write can limit early depth testing or force a conservative execution mode. The exact behavior remains driver- and shader-dependent; it must be measured rather than assumed.

Candidate execution flow

flowchart TD
    A[Shader-state builder] --> B{Native-depth subset admitted?}
    B -->|No| C[Generate current custom-depth shader]
    B -->|Yes| D[Generate matching native-depth vertex/raster mapping]
    D --> E[Generate fragment shader with no gl_FragDepth write]
    E --> F[Host may perform early depth rejection]
    C --> G[Reference guest-depth route]
    F --> H[Compare output and GPU work]
    G --> H
Loading

Illustrative policy:

bool native_depth_eligible(const ShaderState *s)
{
    return s->psh.depth_format == DEPTH_FORMAT_D16 &&
           !s->psh.z_perspective &&
           !s->geom.polygon_offset_enabled &&
           !s->w_buffer_enabled &&
           !s->depth_readback_sensitive &&
           !s->occlusion_report_sensitive;
}

if (native_depth_eligible(state) &&
    policy == DEPTH_POLICY_NATIVE_EXPERIMENT) {
    options.depth_route = DEPTH_ROUTE_NATIVE;
} else {
    options.depth_route = DEPTH_ROUTE_REFERENCE;
}

The real implementation must derive the exact eligibility conditions from the current guest-depth mapping. The sketch is not a complete accepted guard.

What may disappear

  • Per-fragment floor and encoding operations.
  • The explicit fragment-depth write.
  • Fragment shader invocations for pixels that the host can reject early.
  • Texture reads and register-combiner work for those rejected pixels.

What remains mandatory

  • Correct depth compare direction and range.
  • Correct near/far clipping and depth clamp behavior.
  • Stencil side effects and alpha/discard ordering.
  • Polygon-offset semantics.
  • W-buffer and perspective-depth handling.
  • Guest-visible depth readback.
  • Occlusion/report semantics.
  • Exact reference route for unsupported or numerically sensitive states.

Exact versus approximate subsets

A native route is approximate until proven otherwise. It can graduate to an exact shortcut only for states where the host rasterizer and attachment conversion reproduce guest-visible results over all tested values.

The first experiment should be intentionally narrow:

ordinary Z buffering
opaque geometry
no alpha test/discard
no W buffering
no polygon-offset emulation
no guest depth readback during the measured interval
no occlusion report dependency
one depth format and one host GPU family initially

Expanding several dimensions at once would make numerical differences impossible to attribute.

Why a runtime branch is insufficient

This is the wrong shape:

if (native_depth_enabled) {
    // skip assignment
} else {
    gl_FragDepth = reference_depth;
}

The shader still statically writes fragment depth, so the driver may not recover the intended early-depth behavior. The experiment needs a distinct module/program key and a variant in which no execution path writes gl_FragDepth.

Similarly, a conservative-depth declaration must not be used unless its promise is true for every fragment. A false depth_unchanged, depth_less, or depth_greater declaration can produce undefined depth-test behavior rather than a harmless approximation.

Advantages

  • Can remove whole fragment invocations, not only arithmetic.
  • Benefits scale with overdraw and fragment-shader cost.
  • Uses the existing shared shader-generation boundary.
  • Can remain capability/policy selected with a maintained reference route.
  • Also informs a possible Vulkan experiment because the generator is shared.

Costs and risks

  • One additional shader/program-key dimension increases cache pressure.
  • Numerical mismatches can cause z-fighting, decal failure, or incorrect occlusion.
  • Alpha/discard and stencil interactions can change ordering.
  • A driver may already optimize some shaders, producing little benefit.
  • Higher surface scaling can magnify both the opportunity and visible error.
  • A successful synthetic overdraw test may not represent normal games.

Required counters

native_depth_eligible_draws
native_depth_selected_draws
reference_depth_draws
blocked_w_buffer
blocked_polygon_offset
blocked_alpha_or_discard
blocked_report_or_readback
program_variants_created
fragment_invocations, when the host exposes them
GPU elapsed time for admitted draws

The accounting rule is:

eligible draws = native route + policy-forced reference route

If eligibility is near zero, the experiment has not tested the intended shortcut.

Qualification matrix

Test Required result
High-overdraw opaque layers Lower GPU time or fragment invocation count
One guest-depth-unit separation No unexpected compare reversal
Coplanar/decal geometry Differences identified and scoped
Alpha-tested geometry Reference route or proven ordering
Stencil operations Same final stencil/depth state
Near/far clipping and depth clamp Same accepted/rejected geometry
Occlusion reports Same report values or route blocked
CPU depth readback Same bytes or route blocked before readback
Native and upscaled resolutions No scale-dependent unbounded error
Multiple host vendors Capability/result recorded per driver

Acceptance rule

Promote to Researched when a Release build proves frequent route use, lower GPU work or tail time, and a clearly bounded numerical compatibility envelope. Promotion to Implemented requires a guarded fallback and title/retail coverage appropriate to the admitted state.

Rejection rule

Stop or narrow the route when it only changes depth output without reducing fragment work, when the allowed subset is rarely reached, or when compatibility depends on undeclared title-specific behavior.

Related

Clone this wiki locally