Skip to content

OpenGL Uniform Epoch Tracking

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

OpenGL Uniform Epoch Tracking

Lifecycle: Performance - Unproven · Type: Prepared-state and upload shortcut
Reviewed source: 322986f4c5502b3fa887a9605ac8cd88e9b2c579
State: The clean shader-selection path still calls the full uniform-update routine. Runtime frequency and cost are unmeasured.

OpenGL candidate directory · OpenGL Renderer · Vulkan Uniform Update Plans

Purpose

pgraph_gl_bind_shaders() already has a shader-selection fast path. When the current binding still matches all shader dependencies, it skips cache lookup, module selection, linking, and program switching. It then jumps directly to update_uniforms.

That label still calls update_shader_uniforms() for the active program. The uniform routine reconstructs values and issues glUniform*() calls for active locations. Therefore, a clean shader hit does not imply a clean uniform path.

The candidate adds per-program, per-dependency uniform generations so unchanged groups bypass both value preparation and OpenGL upload calls.

Source seams

  • hw/xbox/nv2a/pgraph/gl/shaders.c
    • pgraph_gl_bind_shaders()
    • update_shader_uniforms()
    • update_shader_uniform_locs()
    • shader/program cache entry lifetime
  • Shared uniform source state in PGRAPH and the register-write paths that mutate it.
  • Texture scale, surface scale, inline/uniform attributes, polygon offset, fog, viewport/clip inputs, and program-specific uniform locations.

Current fast-path shape:

if (r->shader_binding &&
    !pgraph_glsl_check_shader_state_dirty(pg,
                                          &r->shader_binding->state)) {
    nv2a_profile_inc_counter(NV2A_PROF_SHADER_BIND_NOTDIRTY);
    goto update_uniforms;
}

/* shader selection work */

update_uniforms:
    update_shader_uniforms(pg, r->shader_binding);

Current execution flow

flowchart TD
    A[Draw begins] --> B{Shader state dirty?}
    B -->|Yes| C[Lookup/create shader binding and program]
    B -->|No| D[Retain current shader program]
    C --> E[Build active uniform values]
    D --> E
    E --> F[Iterate active uniform locations]
    F --> G[Call glUniform* for active entries]
    G --> H[Draw]
Loading

The clean route saves shader work but converges before uniform preparation.

Candidate execution flow

flowchart TD
    A[Program selected] --> B[Read current dependency generations]
    B --> C{Program snapshot matches all active groups?}
    C -->|Yes| D[Skip value construction and GL upload]
    C -->|No| E[Identify dirty groups]
    E --> F[Build only required values]
    F --> G[Upload only changed active locations or blocks]
    G --> H[Publish program-local generation snapshot]
    D --> I[Draw]
    H --> I
Loading

Illustrative structures:

typedef enum GLUniformGroup {
    GL_UNIFORM_GROUP_VSH_CONSTANTS,
    GL_UNIFORM_GROUP_PSH_COMBINERS,
    GL_UNIFORM_GROUP_TEXTURE_SCALE,
    GL_UNIFORM_GROUP_INLINE_ATTRIBUTES,
    GL_UNIFORM_GROUP_VIEWPORT_DEPTH,
    GL_UNIFORM_GROUP_FOG,
    GL_UNIFORM_GROUP_POLYGON_OFFSET,
    GL_UNIFORM_GROUP_COUNT,
} GLUniformGroup;

typedef struct GLProgramUniformState {
    uint64_t uploaded_epoch[GL_UNIFORM_GROUP_COUNT];
    bool valid[GL_UNIFORM_GROUP_COUNT];
} GLProgramUniformState;

Update shape:

for (unsigned group = 0; group < GL_UNIFORM_GROUP_COUNT; group++) {
    if (!program->uniform_state.valid[group] ||
        program->uniform_state.uploaded_epoch[group] !=
            pg->uniform_epoch[group]) {
        build_and_upload_group(pg, program, group);
        program->uniform_state.uploaded_epoch[group] =
            pg->uniform_epoch[group];
        program->uniform_state.valid[group] = true;
    }
}

Why state must be per program

A single global “uniforms clean” bit fails when switching programs:

program A uploads constants
program B uploads different locations/layout
program A becomes current again

The guest values may be unchanged, but program A’s OpenGL uniform storage remains separate from program B. Each linked/binary-loaded program needs its own last-uploaded snapshot.

The initial upload after program creation, binary loading, relink, context recreation, or location refresh must be forced regardless of guest epochs.

Dependency groups

A safe first version should use coarse groups rather than one epoch per individual location. Candidate groups include:

  • Vertex constants and light/transform contexts.
  • Fragment combiner constants.
  • Texture dimensions/scales and surface scaling.
  • Inline attributes represented as shader uniforms.
  • Viewport, clip, depth, and depth-format inputs.
  • Fog inputs.
  • Polygon-offset inputs.
  • Any backend compatibility toggles represented as uniforms.

Each group needs a complete writer inventory. A generation increment belongs at the authoritative mutation point, not inside update_shader_uniforms() after the change has already happened.

Value comparison versus generations

A fallback implementation could rebuild all values and compare them with a cached byte image before uploading. That saves some GL calls but does not save the value-construction and register-reading work the candidate is targeting.

Generations allow:

no dependency changed
    -> do not build structure
    -> do not iterate locations
    -> do not call GL

For difficult dependencies, a value cache may still be useful as a second-level correctness check during development.

UBO follow-on

Packed uniform blocks and range binding are a separate experiment. They may reduce API calls for large changes, but they introduce buffer streaming, alignment, lifetime, and driver tradeoffs. The initial epoch experiment should retain glUniform*() and prove that unchanged work is common before changing representation.

What may disappear

  • Repeated register reads and structure construction for clean groups.
  • Iteration over active uniform metadata.
  • Redundant glUniform*() calls.
  • Driver-side uniform validation for unchanged values.
  • Some CPU tail work on high-draw-count frames.

What remains mandatory

  • First upload for every program object.
  • Program-local state across A → B → A switches.
  • Correct updates after binary-cache loads and relinks.
  • Updates for texture/surface scale and inline attribute changes.
  • Invalidation on reset, snapshot restore, context recreation, and program eviction.
  • Correct handling of inactive locations (-1) and differing program layouts.

Advantages

  • Narrow, source-backed, and measurable.
  • Does not change shader semantics or guest output.
  • Can be introduced one dependency group at a time.
  • Preserves current program cache and uniform upload API.
  • Negative-control tests can prove missing generation updates are detected.

Costs and risks

  • Missing one writer produces stale uniforms and subtle rendering errors.
  • Too many fine-grained atomics/counters can erase the CPU saving.
  • Program-local snapshots increase cache-entry state.
  • Coarse epochs may still rebuild more than necessary.
  • A UBO rewrite performed simultaneously would make attribution difficult.

Required counters

uniform_update_calls
uniform_group_checks{group}
uniform_group_hits{group}
uniform_group_rebuilds{group}
uniform_gl_calls{type}
uniform_values_uploaded
uniform_bytes_equivalent
program_first_uploads
program_switch_reuploads
forced_invalidations{reason}
uniform_prepare_us
uniform_upload_us

Accounting:

active group checks = cache hits + rebuilds

Qualification matrix

Scenario Required result
Same program and constants over many draws High hit rate, no GL uniform calls
Change one vertex constant Only VSH group uploads
Change one combiner constant Only PSH group uploads
Texture/surface scale change Relevant scale groups upload
Inline attribute change Correct current value reaches shader
Program A → B → A Each program restores its own required values
New link/program-binary load Forced first upload
Shader-cache eviction and recreation Old snapshot not reused
Snapshot restore/reset/context recreation All affected groups invalidated
Intentionally omit one epoch increment in test build Output oracle detects stale state

Acceptance rule

Promote to Researched when counters show a high clean-group rate in real workloads, output/hash testing remains identical, and an uninstrumented Release build reduces draw-preparation CPU or frame tails.

Rejection rule

Reject or reduce scope when active uniforms change almost every draw, when generation maintenance costs approach the work saved, or when complete writer coverage cannot be established.

Related

Clone this wiki locally