Skip to content

OpenGL Deferred Occlusion Queries

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

OpenGL Deferred and Pooled Occlusion Queries

Lifecycle: Performance - Unproven · Type: Query-lifetime and result-scheduling optimization
Reviewed source: 322986f4c5502b3fa887a9605ac8cd88e9b2c579
State: Per-draw query creation, blocking result retrieval, and deletion are source-confirmed. Safe deferral windows have not been established.

OpenGL candidate directory · OpenGL Renderer · Approximate Occlusion Reports

Purpose

When z-pass pixel counting is enabled, the OpenGL draw path grows an array, creates a new query object, and begins GL_SAMPLES_PASSED for each query-enabled draw. Report processing later retrieves every result with GL_QUERY_RESULT, scales each result, accumulates it, deletes the query objects, and writes the guest report.

The candidate has two exact components:

  1. Reuse query objects from a bounded pool instead of creating/deleting them per draw.
  2. Delay blocking result retrieval until the guest ordering contract requires the value, while polling availability and allowing unrelated work to continue.

This is separate from substituting approximate visibility results. The reference result remains exact.

Source seams

  • hw/xbox/nv2a/pgraph/gl/draw.c
    • per-draw glGenQueries() and glBeginQuery(GL_SAMPLES_PASSED)
    • glEndQuery() at draw end
  • hw/xbox/nv2a/pgraph/gl/reports.c
    • process_pending_report()
    • glGetQueryObjectuiv(..., GL_QUERY_RESULT, ...)
    • scale-factor division, accumulation, deletion, and guest report write
  • Guest report queue and ordering semantics in common PGRAPH report handling.

Current execution flow

flowchart TD
    A[Query-enabled draw begins] --> B[grow query ID array]
    B --> C[glGenQueries]
    C --> D[glBeginQuery GL_SAMPLES_PASSED]
    D --> E[Draw]
    E --> F[glEndQuery]
    F --> G[Attach query IDs to guest report]
    G --> H[Process pending report]
    H --> I[glGetQueryObject GL_QUERY_RESULT]
    I --> J[Potential CPU wait]
    J --> K[Divide each result by scale²]
    K --> L[Accumulate]
    L --> M[glDeleteQueries]
    M --> N[Write guest DMA report]
Loading

Candidate execution flow

flowchart TD
    A[Query-enabled draw] --> B[Acquire query ID from free pool]
    B --> C[Begin/end exact host query]
    C --> D[Attach query record to guest report boundary]
    D --> E{Guest needs report value now?}
    E -->|No| F[Poll GL_QUERY_RESULT_AVAILABLE during pending processing]
    F --> G{Available?}
    G -->|No| H[Continue unrelated eligible work]
    G -->|Yes| I[Retrieve exact result]
    E -->|Yes| J[Wait only for required report queries]
    J --> I
    I --> K[Scale/accumulate using current exact ordering]
    K --> L[Write guest report]
    L --> M[Return query IDs to pool after result consumption]
Loading

Illustrative pool record:

typedef enum GLQueryState {
    GL_QUERY_FREE,
    GL_QUERY_ACTIVE,
    GL_QUERY_PENDING_RESULT,
    GL_QUERY_ATTACHED_TO_REPORT,
} GLQueryState;

typedef struct GLQuerySlot {
    GLuint id;
    GLQueryState state;
    uint64_t draw_sequence;
    uint64_t report_sequence;
} GLQuerySlot;

Guest ordering questions

Deferral is safe only while the guest does not require the report value. The implementation must identify:

  • when GET_REPORT queues a result;
  • when later PFIFO/CPU behavior depends on report completion;
  • clear boundaries that reset accumulated counts;
  • report DMA writes and bounded completion behavior;
  • teardown/snapshot requirements.

If the current common path treats report processing as a required barrier, the first experiment can still pool objects while retaining the same blocking point. Deferral can then be measured separately.

Scale-factor rounding trap

Current code divides each individual query result by surface_scale_factor² before adding it:

for each query q:
    result = exact(q);
    result /= scale * scale;
    accumulated += result;

Combining several draws into one larger host query changes rounding:

floor(a / k) + floor(b / k) != floor((a + b) / k)

Therefore, “one query for many draws” is not automatically equivalent, even when report/clear boundaries appear compatible. Pooling IDs does not require combining results and is the safer first step.

Query availability

Polling should avoid a blocking result read:

GLuint available = GL_FALSE;
glGetQueryObjectuiv(slot->id, GL_QUERY_RESULT_AVAILABLE, &available);
if (!available) {
    return REPORT_NOT_READY;
}

glGetQueryObjectuiv(slot->id, GL_QUERY_RESULT, &samples);

Availability polling itself has driver cost. It should occur at bounded low-frequency processing points, not inside a tight loop.

Pool design

A bounded pool needs:

  • initial capacity and maximum capacity;
  • free list;
  • active/pending/report-owned states;
  • query context ownership;
  • fallback when no free object is available;
  • reset after context recreation;
  • teardown that waits or safely abandons pending queries according to GL rules.

The pool should not recycle an ID until its result has been consumed and no report record references it.

What may disappear

  • Per-draw glGenQueries() and glDeleteQueries() calls.
  • Heap reallocations for growing query arrays, if records use a prepared container.
  • Blocking GL_QUERY_RESULT reads before the guest needs them.
  • CPU/GPU pipeline stalls caused by eager result retrieval.

What remains mandatory

  • Exact GL_SAMPLES_PASSED semantics for admitted reports.
  • Per-query scale division and accumulation order unless proven otherwise.
  • Clear/report boundary behavior.
  • Timely guest DMA report publication when required.
  • Query lifetime until result consumption.
  • Bounded PCI/report completion behavior.
  • Reference blocking route when ordering requires completion.

Advantages

  • Pooling is narrow and exact.
  • Deferral can expose useful CPU/GPU overlap.
  • Existing report queue provides a natural ownership boundary.
  • Counters can distinguish allocation cost from result-wait cost.
  • Keeps approximate-report experiments separate.

Costs and risks

  • Availability polling can add repeated driver calls.
  • Guest ordering may require results sooner than expected.
  • Query IDs can leak or be reused while referenced.
  • Combining queries can change rounded guest results.
  • A small pool can reintroduce waits; an unbounded pool can grow indefinitely.
  • Context reset/snapshot handling becomes more complex.

Required counters

query_enabled_draws
query_pool_acquires
query_pool_reuses
query_objects_created
query_pool_exhaustions
query_result_availability_polls
query_results_ready_without_wait
query_result_blocking_reads
query_result_wait_us
queries_per_guest_report
query_objects_deleted
report_completion_latency_us

Qualification matrix

Scenario Required result
One query per report Exact reference value
Multiple queries before report Same per-query scaled sum
Clear between draws/reports Correct reset boundary
Surface scale 1x–4x Identical integer report value
Delayed GPU No premature ID reuse
Small forced pool Correct fallback or bounded wait
Report requested immediately Required blocking path correct
Report can be deferred Unrelated work continues safely
Snapshot/reset/context recreation Pending IDs retired or invalidated
Retail titles using reports No logic/visibility regressions

Acceptance rule

Promote query pooling when object churn is frequent and exact tests pass. Promote deferred retrieval separately only when route evidence shows blocking waits before the guest requires data and Release measurements improve useful overlap without delaying required reports.

Rejection rule

Reject deferral if guest ordering requires immediate completion in common cases or if availability polling costs as much as the waits avoided. Do not replace exact reports with a guessed result under this page’s scope.

Related

Clone this wiki locally