Repository navigation
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
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.
-
hw/xbox/nv2a/pgraph/gl/draw.cpgraph_gl_draw_begin()pgraph_gl_clear_surface()
-
hw/xbox/nv2a/pgraph/gl/surface.c- render-to-texture helper changes viewport, masks, tests, FBO, VAO, program, polygon mode, texture state
-
hw/xbox/nv2a/pgraph/gl/display.c- separate display context and display-render state
- Texture and sampler binding code.
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
The next guest draw simply reissues its expected state, which is safe but potentially expensive.
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
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;
}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.
Internal helpers call the same state-application functions as guest draws. The shadow remains accurate and restores become explicit.
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.
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.
A staged implementation should isolate groups:
- Color/depth/stencil write masks.
- Blend enable/function/equation/color.
- Cull/front face/polygon mode.
- Depth/stencil enable/functions/operations.
- Dither, point, line, polygon smoothing.
- Viewport/scissor.
- 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.
- 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.
- 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.
- 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.
- 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.
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.
| 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 |
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.
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.
DSP tooling guide and navigation updated November 2, 2026 for experimental PR #384, source 8f51d237. Capture/replay usage and limits.
The earlier wiki-wide review was published November 1, 2026. Emulator source: b7140848. Reviewed wiki content: 1d2b671. Current status · Release availability · Publication process.
- Home
- Current status
- Stable versus main
- Implemented mechanisms
- Controls and defaults
- Measured results and limits
- Complete change register
- System
- CPU and vCPU
- Memory and DMA
- Graphics / NV2A
- APU and audio
- Timing and timers
- Platform and I/O
- Hardware observations
- Fidelity boundaries
- Methodology
- PGR2 routes
- Debugging lag / hot paths
- Tools and commands
- DSP capture, replay and fuzzing
- HLE readiness and public XISO proof
- Concurrent DSP shadow and OpenAL proof
- DSP HLE operation inventory
- DSP HLE fix examples and testing
- Development
- Upstream shortlist
- Wiki maintenance
- Research / retired work
- Optimization catalog
- Hot-path shortcuts
- Researched experiments
- Unproven proposal directory
- PR lessons
- September26 snapshot
Repository · PRs · Issues