Skip to content

OpenGL Renderer

OpenAI Codex edited this page Nov 1, 2026 · 5 revisions

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.

Backend role

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]
Loading

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.

Initialization and ownership

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.

Draw path

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]
Loading

Which stages do real work depends on dirty flags, cache hits, draw mode, and the current GL state.

Shader path

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.

Why a shader bind is more than compilation

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.

State model

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.

Textures

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.

Surfaces

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.

Vertex and index data

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.

Synchronization

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.

Pending surface downloads

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.

Reports and queries

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.

Display and framebuffer

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.

Save/restore and shutdown

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:

  1. No producer will issue more backend work.
  2. Pending guest-visible operations are resolved or safely discarded according to lifecycle rules.
  3. GL objects are destroyed while the correct context is valid.
  4. The context is destroyed last enough that object teardown remains legal.

Main source files

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

Common performance regions

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

Correctness traps

Implicit driver behavior is still behavior

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.

Context ownership

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.

Cache identity

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.

Readback ordering

A GPU surface used later as CPU-visible data must be synchronized before CPU-side decoding/consumption, not merely before the final draw call.

Host-driver behavior is part of the emulator path

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]
Loading

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.

Fidelity-sensitive debugging questions

  • 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?

OpenGL as a control backend

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.

Performance candidates

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.

Current correctness and shared scheduling

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.

Related pages

Current vertex and failure-side contracts

Merged PR #125 adds two durable rules.

Reconcile newest representation before CPU-side decode

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.

Draw-scope lifecycle is monotonic

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.

Clone this wiki locally