Skip to content

OpenGL Render State Shadow

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

OpenGL Render-State Shadow

Lifecycle: Performance - Unproven · Type: State-call suppression and invalidation architecture
Reviewed source: 322986f4c5502b3fa887a9605ac8cd88e9b2c579
State: pgraph_gl_draw_begin() reissues broad state groups for each non-noop draw. Some local texture fields are cached, but no complete per-context render-state shadow exists.

OpenGL candidate directory · Sampler objects · OpenGL Renderer

Purpose

The OpenGL draw-begin path applies color/depth/stencil masks, blend enable/function/equation/color, culling, front face, polygon offset disables, depth test/function, depth clamp, provoking vertex, stencil function/operations, dither, point size, line/polygon smoothing, line width, viewport, scissor, and other state on each draw.

Drivers may internally remove redundant work, but xemu still pays function-call, dispatch, validation, and state-translation cost. The candidate maintains the last state applied in each OpenGL context and issues calls only when the effective host state changes.

The hard part is not comparing values. Internal rendering helpers—surface copies, clears, display composition, and other non-guest passes—change OpenGL state outside pgraph_gl_draw_begin(). They must update or invalidate the shadow before the next guest draw.

Source seams

Current execution flow

flowchart TD
    A[Guest draw begins] --> B[Decode guest render registers]
    B --> C[Issue masks]
    C --> D[Issue blend state]
    D --> E[Issue cull/front-face state]
    E --> F[Issue depth/stencil state]
    F --> G[Issue dither/smoothing/line width]
    G --> H[Issue viewport/scissor]
    H --> I[Draw]
    J[Internal surface/display helper] --> K[Changes overlapping GL state]
    K --> A
Loading

The next guest draw simply reissues its expected state, which is safe but potentially expensive.

Candidate execution flow

flowchart TD
    A[Guest or internal pass requests effective state] --> B[Build compact host-state groups]
    B --> C{Shadow group valid and equal?}
    C -->|Yes| D[Skip GL calls for that group]
    C -->|No| E[Issue required GL calls]
    E --> F[Update shadow]
    D --> G[Continue]
    F --> G
    H[Unknown external/internal state mutation] --> I[Invalidate affected groups]
    I --> A
Loading

Illustrative group state:

typedef struct GLRasterShadow {
    bool valid;
    bool cull_enable;
    GLenum cull_face;
    GLenum front_face;
    GLenum polygon_mode;
    bool depth_clamp;
    bool line_smooth;
    bool polygon_smooth;
    float line_width;
} GLRasterShadow;

typedef struct GLContextStateShadow {
    GLRasterShadow raster;
    GLBlendShadow blend;
    GLDepthStencilShadow depth_stencil;
    GLViewportScissorShadow viewport_scissor;
    GLMaskShadow masks;
    GLBindingShadow bindings;
} GLContextStateShadow;

Application helper:

static void apply_depth_state(GLContextStateShadow *s,
                              const GLDepthState *want)
{
    if (!s->depth.valid || s->depth.test_enable != want->test_enable) {
        want->test_enable ? glEnable(GL_DEPTH_TEST)
                          : glDisable(GL_DEPTH_TEST);
        s->depth.test_enable = want->test_enable;
    }
    if (!s->depth.valid || s->depth.func != want->func) {
        glDepthFunc(want->func);
        s->depth.func = want->func;
    }
    if (!s->depth.valid || s->depth.write_enable != want->write_enable) {
        glDepthMask(want->write_enable);
        s->depth.write_enable = want->write_enable;
    }
    s->depth.valid = true;
}

Shadow ownership

There are two shared OpenGL contexts:

  • render context;
  • display context.

Each needs its own state shadow. A state value applied in one context cannot be assumed active in the other, even when objects are shared.

The shadow belongs to the context state machine, not to guest PGRAPH state alone.

Invalidation strategies

Preferred: all writers use the shadow API

Internal helpers call the same state-application functions as guest draws. The shadow remains accurate and restores become explicit.

Transitional: targeted invalidation

When converting a large helper is impractical, invalidate groups it may disturb:

void render_surface_to(...)
{
    gl_shadow_invalidate(ctx,
        GL_STATE_FRAMEBUFFER |
        GL_STATE_PROGRAM |
        GL_STATE_VERTEX_INPUT |
        GL_STATE_TEXTURES |
        GL_STATE_RASTER |
        GL_STATE_DEPTH_STENCIL |
        GL_STATE_BLEND |
        GL_STATE_VIEWPORT_SCISSOR |
        GL_STATE_MASKS);

    /* legacy helper calls */
}

The next guest draw reissues only invalid groups.

Rejected: glGet*() validation per draw

Querying host state on each draw usually replaces redundant setters with synchronous or expensive state reads. It is suitable for optional debug assertions, not the performance path.

State groups

A staged implementation should isolate groups:

  1. Color/depth/stencil write masks.
  2. Blend enable/function/equation/color.
  3. Cull/front face/polygon mode.
  4. Depth/stencil enable/functions/operations.
  5. Dither, point, line, polygon smoothing.
  6. Viewport/scissor.
  7. Program, VAO, buffer, FBO, active texture, image, and sampler bindings.

Start with groups whose current code is clearly repeated and whose internal writers are easy to inventory.

What may disappear

  • Redundant glEnable()/glDisable() calls.
  • Repeated mask, function, equation, viewport, scissor, and line-width calls.
  • Driver validation of unchanged state.
  • Some restore calls after internal passes.
  • CPU overhead on high-draw-count workloads.

What remains mandatory

  • Exact effective host state before every draw/pass.
  • Per-context shadow separation.
  • Invalidation after unknown/external state changes.
  • Correct state after clears, surface copies, display composition, and debugging tools.
  • Full reapply after context recreation/reset.
  • Reference/debug mode that can disable the shadow or verify it.

Advantages

  • Broad potential on CPU-bound draw submission.
  • Can be implemented group by group.
  • No guest semantic approximation when the shadow is complete.
  • Makes internal state ownership explicit.
  • Supports sampler objects and simpler surface-copy paths.

Costs and risks

  • Missing one writer causes state leakage and difficult rendering bugs.
  • The shadow can become another large source of truth unless APIs are centralized.
  • State comparisons themselves cost CPU; grouping and direct generations matter.
  • Internal helpers currently restore only selected state, so partial adoption is dangerous.
  • Debug overlays or third-party frontend code may mutate state outside the renderer.
  • Fewer GL calls do not necessarily improve whole-frame performance.

Required counters

state_requests{group}
state_group_hits{group}
state_group_applies{group}
GL_calls_issued{function}
GL_calls_skipped{function}
shadow_invalidations{group,reason}
forced_full_reapply
context_switches
state_prepare_us
state_apply_us

Debug build checks can periodically compare selected shadow values with glGet*() outside the timed path.

Qualification matrix

Scenario Required result
Many identical-state draws High skip rate, identical output
Change one blend/depth/raster field Only affected calls issued
Guest draw → internal surface copy → guest draw Correct guest state restored/reapplied
Clear with unusual masks/scissor Next draw correct
Display context operation Render-context shadow unaffected
Same texture on different units/samplers Binding shadow correct
Context recreation/reset All groups invalidated
Optional debug state comparison Shadow matches host state at checkpoints
Deliberately omit one invalidation in test build Regression fixture detects state leak
Multiple vendors/drivers Stable correctness and measured call reduction

Acceptance rule

Promote to Researched when a complete writer inventory exists for the implemented groups, stress tests across internal passes remain correct, and Release profiling shows meaningful CPU/tail reduction rather than only fewer API calls.

Rejection rule

Reject or keep only a smaller group subset if writer coverage is too fragile, if invalidations cause nearly full reapplication every draw, or if driver-side redundancy elimination makes the measured benefit negligible.

Related

Clone this wiki locally