Skip to content

OpenGL Asynchronous Display Handoff

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

OpenGL Asynchronous Display Handoff

Lifecycle: Performance - Unproven · Type: Host synchronization architecture
Reviewed source: 322986f4c5502b3fa887a9605ac8cd88e9b2c579
State: The two client-side waits are source-confirmed. Their cost and the feasibility of a fully asynchronous frontend contract remain unmeasured.

OpenGL candidate directory · Completed display reuse · OpenGL Renderer

Purpose

The OpenGL renderer uses separate shared render and display contexts. When the frontend asks for the framebuffer texture, the current path asks the PGRAPH thread to prepare the source, waits for the render context to finish, switches to the display context, renders the display/PVIDEO composition, waits again, then signals the frontend and returns a texture ID.

The candidate keeps producer/consumer ordering but replaces CPU-blocking waits with GPU-side cross-context dependencies and explicit output-slot ownership.

This is not a proposal to remove every glFinish() or every guest-visible completion boundary. It targets waits that exist because the host interface requires an already-completed texture before returning.

Source seams

  • hw/xbox/nv2a/pgraph/gl/display.c
    • gl_fence()
    • pgraph_gl_sync()
    • pgraph_gl_get_framebuffer_surface()
    • display-context composition
  • hw/xbox/nv2a/pgraph/gl/renderer.c
    • render/display context creation
    • pgraph_gl_flip_stall() and its guest-visible glFinish()
  • Frontend framebuffer ownership and presentation interface must be updated to accept readiness and release dependencies, not only a texture ID.

Current fence helper:

static void gl_fence(void)
{
    GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
    int result = glClientWaitSync(
        fence, GL_SYNC_FLUSH_COMMANDS_BIT, 5000000000ULL);
    assert(result == GL_CONDITION_SATISFIED ||
           result == GL_ALREADY_SIGNALED);
    glDeleteSync(fence);
}

The five-second value is a maximum timeout, not evidence of a five-second normal delay.

Current execution flow

sequenceDiagram
    participant UI as Frontend/UI
    participant PF as PGRAPH/render context
    participant DC as Display context
    participant GPU as Host GPU

    UI->>PF: request framebuffer texture
    PF->>PF: upload/prepare source surface
    PF->>GPU: fence render-context work
    PF->>PF: glClientWaitSync
    PF->>DC: switch context
    DC->>GPU: render display/PVIDEO composition
    DC->>DC: glClientWaitSync
    DC->>PF: switch context back
    PF-->>UI: signal sync_complete + texture ID
Loading

The CPU does not only submit ordering; it waits for both stages before publishing the result.

Candidate execution flow

sequenceDiagram
    participant UI as Frontend/UI
    participant PF as Render context
    participant DC as Display context
    participant GPU as Host GPU

    PF->>GPU: render source
    PF->>GPU: create render_ready fence + glFlush
    PF->>DC: enqueue display job with source and render_ready
    DC->>GPU: glWaitSync(render_ready)
    DC->>GPU: compose into free output slot
    DC->>GPU: create display_ready fence + glFlush
    DC-->>UI: publish {texture, display_ready, slot_generation}
    UI->>GPU: wait/import dependency at consumption boundary
    UI-->>DC: publish consumer_finished for slot
    DC->>DC: recycle slot after consumer completion
Loading

Illustrative interfaces:

typedef struct GLDisplayFrame {
    GLuint texture;
    GLsync ready;
    uint64_t slot_generation;
    unsigned width;
    unsigned height;
} GLDisplayFrame;

GLDisplayFrame pgraph_gl_acquire_display_frame(void);
void pgraph_gl_release_display_frame(
    uint64_t slot_generation, GLsync consumer_finished);

An implementation may use explicit server waits in the frontend context rather than passing raw sync ownership across arbitrary threads. The contract is the important part: readiness and lifetime travel with the image.

What may disappear

  • CPU time inside the first glClientWaitSync().
  • CPU time inside the display-context completion wait.
  • Render-thread/frontend round trips caused only by the “ready texture ID” interface.
  • GPU idle periods caused by preventing later independent render work from being queued.

What remains mandatory

  • The display pass must not sample the source before rendering/upload completes.
  • The frontend must not sample a display output before composition completes.
  • The renderer must not overwrite or destroy the source while display composition uses it.
  • The display context must not overwrite an output slot while the frontend uses it.
  • Guest-visible flip-stall behavior must retain its real completion semantics.
  • Resize, reset, shutdown, snapshot, and context loss must drain or abandon jobs safely.

Output-slot model

A practical design needs at least two or three display output slots:

FREE
  -> RECORDING
  -> SUBMITTED
  -> READY_FOR_FRONTEND
  -> FRONTEND_IN_USE
  -> RETIRING
  -> FREE

Each state transition needs a generation or ticket. A texture name alone is insufficient because the same texture object may be recycled for a later frame.

The source surface also needs a retained reference or version. The display job must not rely on “same address” as proof that the source is still the same image.

Advantages

  • Preserves GPU ordering without forcing the application CPU to wait immediately.
  • Allows render work, display composition, and frontend presentation to overlap.
  • Makes ownership explicit rather than hidden in synchronous behavior.
  • Creates the foundation needed for completed-output reuse and a future native presentation path.
  • Keeps guest-visible completion waits separate from host-interface waits.

Costs and risks

  • Requires a framebuffer interface change across renderer and frontend.
  • Adds slot, fence, and teardown state.
  • Shared-context sync behavior must be tested on every supported platform/driver family.
  • glWaitSync() still serializes dependent GPU work; it only removes the client wait.
  • A ring that is too small can reintroduce waits under another name.
  • A ring that is unbounded can hide a leak or create latency growth.
  • Immediate surface reuse, resize, or context recreation can expose stale references.

Failure examples

Source overwritten too early

render red into surface A
queue display composition of A
render blue into reused A before display samples it
frontend unexpectedly sees blue

Output slot reused too early

display frame N into slot 0
frontend still sampling slot 0
renderer composes frame N+2 into slot 0
tearing or mixed image

False performance result

Replacing glClientWaitSync() with glWaitSync() but immediately waiting for a free output slot can move the same stall from the fence helper into slot acquisition. Evidence must measure total useful overlap.

Required counters

render_to_display_client_wait_calls
render_to_display_client_wait_us
display_to_frontend_client_wait_calls
display_to_frontend_client_wait_us
server_waits_enqueued
display_slots_in_use_peak
slot_reuse_waits
source_lifetime_waits
frames_published
frames_consumed
frames_dropped_or_replaced
presentation_latency_us
fixed guest progress and host frame p95/p99

Qualification matrix

Scenario Required result
Artificially delayed render context Display waits correctly without stale source
Artificially delayed frontend Output slot is not overwritten
Alternating red/blue frames Exact frame/version observed
Host display faster than guest No unnecessary recomposition or stale publication
Host display slower than guest Defined queue/drop/backpressure behavior
Resize/fullscreen/minimize Old slots retired before destruction
PVIDEO enable/disable Overlay state belongs to correct frame
Snapshot save/load Pending jobs drained or invalidated
Renderer reset/shutdown No wait cycle or leaked sync object
Guest flip stall Required guest completion preserved

Acceptance rule

Promote to Researched only when route counters show meaningful client-wait time, the asynchronous version preserves frame identity under delayed producer/consumer tests, and Release measurements improve useful guest progress or CPU/tail behavior without growing latency or memory unboundedly.

Rejection rule

Reject or redesign when waits merely move to slot acquisition, when the frontend cannot safely consume readiness dependencies, or when preserving source/output lifetimes removes the expected overlap.

Related

Clone this wiki locally