Repository navigation
OpenGL Renderer
Reviewed: November 1, 2026 · Source: main
b7140848. Release availability: OpenGL, #125 vertex/lifecycle repair, #127 failure cleanup and the #264/#265/#267/#271 shared graphics corrections are in v0.8.136-0-g458730bf5373 (September 30, 2026). #256 cubemap mip cropping and #298 DMA-source revalidation are reviewed-main only.
The OpenGL backend realizes shared NV2A/PGRAPH state through a stateful OpenGL context. It is not a separate guest GPU model: PFIFO, PGRAPH, guest shader semantics, surfaces, reports, and resource ownership remain NV2A concepts before this backend translates them into GL operations.
See NV2A Graphics Architecture first for the shared path.
flowchart TD
A[PGRAPH guest state] --> B[OpenGL renderer]
B --> C[Resolve surfaces]
B --> D[Resolve textures]
B --> E[Generate / bind shaders]
B --> F[Prepare vertex/index data]
C --> G[Update GL state]
D --> G
E --> G
F --> G
G --> H[glDraw*]
H --> I[Mark guest-visible results / dirty surfaces]
OpenGL keeps substantial host state implicit inside the current GL context and driver. xemu tracks enough information to avoid redundant work and to preserve guest behavior across that stateful API.
The backend entry point is hw/xbox/nv2a/pgraph/gl/renderer.c.
pgraph_gl_init() allocates the backend state and establishes an offscreen GL context before initializing the major renderer subsystems.
The initialization sequence includes work equivalent to:
allocate PGRAPHGLState
↓
create / make current offscreen GL context
↓
initialize debugging
↓
initialize shaders
↓
initialize surfaces
↓
initialize textures
↓
initialize buffers / framebuffer support
↓
initialize display / compute support
↓
perform initial guest-memory synchronization
Backend finalization tears these resources down and destroys the context after renderer work has been quiesced.
The GL context is a major ownership boundary. OpenGL objects and calls depend on a valid current context; they cannot be freely mutated from arbitrary worker threads without an explicit context-sharing/ownership design.
hw/xbox/nv2a/pgraph/gl/draw.c contains the backend draw preparation and issue path.
A representative draw requires more than glDraw*:
flowchart TD
A[PGRAPH draw] --> B[Update / validate surface state]
B --> C[Prepare textures]
C --> D[pgraph_gl_bind_shaders]
D --> E[Prepare vertex and index sources]
E --> F[Apply GL raster/depth/blend/etc state]
F --> G[Issue GL draw]
G --> H[Record resulting dirty/state changes]
Which stages do real work depends on dirty flags, cache hits, draw mode, and the current GL state.
The shared NV2A shader/combiner layer generates shader source from guest state. The OpenGL backend then compiles/links or reuses GL shader/program state.
Conceptually:
NV2A ShaderState
↓
shared GLSL generation
↓
OpenGL shader/program lookup
├─ hit → reuse program/binding
└─ miss → compile/link host GL program
↓
update uniforms / bindings
↓
use program for draw
gl/shaders.c owns backend-specific shader/program binding, including pgraph_gl_bind_shaders(), and xemu program-binary persistence. Reload validates xemu version, GL vendor/version and binary format; this is distinct from Vulkan SPIR-V, learned-family and driver caches.
A shader-binding path can perform:
- Guest-state comparison.
- Cache lookup.
- Program selection.
- New shader/program creation on a miss.
- Uniform/layout decisions.
- Texture-dependent shader input updates.
- GL program binding.
Therefore a profile that names shader binding does not imply every call compiled a new shader.
OpenGL is a stateful host API. xemu translates NV2A state into GL state such as:
- Viewport/scissor.
- Depth and stencil behavior.
- Blending and color masks.
- Primitive topology.
- Textures and samplers.
- Shader program and uniforms.
- Vertex array/buffer state.
- Framebuffer/surface state.
A backend optimization often consists of proving that a given GL state update is redundant. The proof must use effective guest state and host ownership, not just whether one guest register was dirty.
Texture preparation is implemented primarily under gl/texture.c together with shared PGRAPH texture state.
A safe cached texture identity can depend on:
- Guest DMA/source mapping.
- Texture format and dimensions.
- Palette state where applicable.
- Guest memory dirty state.
- Surface overlap/freshness.
- Sampler state.
- Backend conversion/host format.
A clean dirty bit alone is insufficient if the guest changes the DMA object or another identity input without changing the underlying bytes in the way a simpler cache expects.
gl/surface.c manages the mapping between guest render targets and host GL resources.
A surface may contain data newer than guest VRAM after the host GPU renders to it. If the guest CPU or another GPU path needs those bytes, xemu must first make the GPU-authored result available in guest memory.
Host GL surface newer than VRAM
↓
Guest path requests overlapping bytes
↓
Synchronize / download surface if required
↓
Update guest-visible memory ownership
Avoiding a necessary readback causes stale guest data. Triggering an unnecessary one can cause a large synchronization cost.
OpenGL vertex handling is under gl/vertex.c and draw code. Guest vertex arrays, inline vertices, and index data are translated into host-consumable representations.
The backend must account for:
- Guest DMA bounds.
- Vertex format conversion.
- Stride and element range.
- Inline/uniform attributes.
- Data modified by CPU or by overlapping GPU surfaces.
The current OpenGL path and Vulkan path do not have identical synchronization mechanics even though they must produce the same guest-visible draw.
OpenGL leaves many dependencies implicit to the driver, but xemu still has explicit synchronization points.
Current renderer lifecycle includes operations such as glFinish() in full flush/sync paths. These are strong synchronization points: they wait for previously issued GL work to complete.
A simplified full flush behaves like:
finish outstanding GL work
↓
flush/download required guest surfaces
↓
mark texture/memory mirrors dirty as needed
↓
refresh guest-memory-backed backend data
Strong synchronization is correct but can be expensive. Removing it requires proving what guest-visible dependency made it necessary and replacing that dependency with a narrower equivalent.
The renderer can queue or process downloads outside the immediate guest call. pgraph_gl_process_pending_downloads() is one example of backend work that must be coordinated with PGRAPH state and synchronization.
This is why the shared renderer interface includes process_pending: some operations are logically caused by guest commands but serviced at a safe backend/thread boundary.
OpenGL report/query behavior ultimately writes guest-visible results through shared PGRAPH report semantics.
The host API can provide completion/query mechanisms different from Vulkan's command-buffer/fence model. This can make the two backends show very different timing around the same guest progress marker.
Do not infer “OpenGL is faster” from a guest report or display-write cadence without proving equivalent host GPU completion. A backend may publish a guest-visible progress marker at a different point in its host submission/completion model.
The GL backend owns a host-side framebuffer/display path suitable for presenting the emulated Xbox output. The renderer interface exposes framebuffer surface information to the rest of xemu.
OpenGL also participates in Vulkan presentation on systems where Vulkan rendering shares external memory with an OpenGL presentation context. In that configuration OpenGL presentation context ownership remains distinct from Vulkan's rendering device.
Host GL objects are derived state. Snapshot serialization must preserve guest-visible state and make any GPU-authored guest data coherent before save.
Shutdown must ensure:
- No producer will issue more backend work.
- Pending guest-visible operations are resolved or safely discarded according to lifecycle rules.
- GL objects are destroyed while the correct context is valid.
- The context is destroyed last enough that object teardown remains legal.
| File | Responsibility |
|---|---|
gl/renderer.c |
Backend lifecycle, GL context, flush/sync, pending work, renderer registration |
gl/draw.c |
Draw preparation and GL draw issue |
gl/shaders.c |
Shader/program cache and pgraph_gl_bind_shaders()
|
hw/xbox/nv2a/pgraph/glsl/ |
Shared NV2A vertex/fragment GLSL generation; backend integration lives in gl/shaders.c
|
gl/texture.c |
Texture creation/cache/update |
gl/surface.c |
Render target/surface ownership and readback |
gl/vertex.c |
Guest vertex/index translation and upload |
gl/reports.c |
Backend report/query integration |
gl/display.c |
Display/presentation-facing GL work |
gl/renderer.h |
Backend-owned state and declarations |
| Region | What to prove before optimizing |
|---|---|
| Shader binding | Cache miss versus steady-state state/uniform work |
| Texture binding | Whether cached object still matches DMA/source/effective state |
| Surface download | Whether GPU copy is actually newer and overlapping |
glFinish() |
Which guest-visible dependency requires full completion |
| State updates | Whether driver call is redundant for the effective host state |
| Vertex conversion/upload | Whether conversion/copy can be avoided without stale data |
OpenGL can hide pipeline compilation, validation, resource migration, and synchronization in the driver. A profile moving out of xemu code into the host GL driver does not mean the work disappeared.
Moving compilation or resource creation to another thread is not automatically legal. The worker needs an appropriate GL context/sharing model and object-lifetime contract.
Removing a guest field from a cache key is valid only if that field cannot change generated code, host resource interpretation, or observable output for that cache layer.
A GPU surface used later as CPU-visible data must be synchronized before CPU-side decoding/consumption, not merely before the final draw call.
OpenGL hides substantial work inside the host driver. That work can include validation, internal shader specialization, memory migration and synchronization. It is not NV2A hardware behavior even when it is triggered by a real NV2A state transition.
flowchart LR
A[NV2A state change] --> B[xemu GL translation]
B --> C[OpenGL API call]
C --> D[Host GL driver]
D --> E[Host GPU command/resource work]
E --> F[Guest-visible surface/report result]
A fast path that suppresses a redundant GL call can be a valid emulator optimization because the original Xbox never had that host-driver call. The proof is that the effective NV2A state and resulting guest output remain unchanged.
Conversely, glFinish() is a host-side mechanism used to establish a guest-required completion dependency. Replacing it requires preserving the dependency, not preserving the API call itself.
- Is this GL state update required by changed NV2A state or only by xemu's host-state bookkeeping?
- Is a driver stall caused by an unavoidable guest synchronization point or by overly broad xemu synchronization?
- Is a surface readback required because guest RAM is stale, or merely because overlap tracking is conservative?
- Did a cache optimization change guest output or only remove redundant host work?
Because OpenGL and Vulkan share the guest-facing PGRAPH model, OpenGL is valuable as a regression/control backend during Vulkan changes. It can help answer:
- Is the problem shared guest-state logic or Vulkan-specific host logic?
- Does a test fail identically in both backends?
- Does guest progress differ because of backend synchronization rather than CPU/game behavior?
It is not a perfect oracle: the backends can have independent bugs, and driver behavior differs.
Source-backed OpenGL optimization research is indexed at OpenGL Performance Candidates. Each listed mechanism has its own detailed technical page and remains Unproven until runtime evidence exists.
Merged #125 fixes GPU-surface-to-vertex freshness and draw-scope lifecycle. Checked complete consumed spans are reconciled before CPU decode or mirror refresh, including constant attributes; invalid DMA/VRAM spans reject the segment. This is implemented correctness, not unresolved render-to-vertex research. The contract is detailed below.
Merged #127 unbinds failed texture targets, releases stage references and preserves dirty state for retry. Main-only #298 revalidates texture and palette DMA sources even when a DMA handle is unchanged: backing memory and mapping identity may have changed. Main-only #256 crops applicable bordered cubemap mip levels using their own dimensions. It does not claim every compressed-border case is repaired.
Shared shader #264 implements DOT_ZW depth replacement, and #265 removes the old issue #149 suppression workaround. Ghoulies is a rendering-semantic correction, separate from the optional Accurate NV20 vertex arithmetic diagnostic. Issue #149 is closed; closure does not independently complete the last requested frame-exact transient comparison.
Shared #267 reconciles PTIMER semantics/scheduling; #271 keeps PGRAPH interrupt/control handling from waiting behind renderer work. The older closed/unmerged #81 PTIMER experiment and its adverse historical OpenGL Morrowind result are retained research, not today's implementation status. Def Jam progress observations were Windows Vulkan evidence, so do not generalize them into an OpenGL performance result.
Vulkan PRs may use unchanged OpenGL as a control, but equal guest progress markers do not guarantee equivalent host GPU completion. Both renderers still require title/driver-specific correctness checks.
Merged PR #125 adds two durable rules.
For vertex fetches, compute the complete consumed span first. If a GPU-authored surface overlaps it, reconcile that surface before refreshing the GL vertex mirror or decoding constant attributes. Invalid DMA/VRAM spans reject the segment instead of using stale bytes.
A BEGIN/END scope can flush several segments. Once any segment has submitted, a later rejected or empty segment must not erase that fact.
EMPTY -> REJECTED is still no submission
REJECTED -> SUBMITTED becomes submitted
SUBMITTED -> REJECTED remains submitted
SUBMITTED -> EMPTY remains submitted
Latch query ownership, target, and write masks at begin and consume them once at end.
Merged PR #127 adds the failure-side rule: on invalid texture source layout or generation failure, unbind affected targets, release retained stage references, and leave guest dirty state set for retry. Failure cleanup is part of fast-path correctness.
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