Files
intel-hard-graphics/src/gallium/drivers/asahi/agx_query.c
T
2026-06-06 13:25:15 +03:00

642 lines
17 KiB
C

/*
* Copyright 2022 Alyssa Rosenzweig
* Copyright 2019-2020 Collabora, Ltd.
* SPDX-License-Identifier: MIT
*/
#include <stdint.h>
#include "pipe/p_defines.h"
#include "util/bitset.h"
#include "util/macros.h"
#include "util/ralloc.h"
#include "util/u_dump.h"
#include "util/u_inlines.h"
#include "agx_abi.h"
#include "agx_bo.h"
#include "agx_device.h"
#include "agx_state.h"
#include "libagx.h"
#include "libagx_dgc.h"
#include "libagx_shaders.h"
static bool
is_occlusion(struct agx_query *query)
{
switch (query->type) {
case PIPE_QUERY_OCCLUSION_COUNTER:
case PIPE_QUERY_OCCLUSION_PREDICATE:
case PIPE_QUERY_OCCLUSION_PREDICATE_CONSERVATIVE:
return true;
default:
return false;
}
}
static bool
is_timer(struct agx_query *query)
{
switch (query->type) {
case PIPE_QUERY_TIMESTAMP:
case PIPE_QUERY_TIME_ELAPSED:
return true;
default:
return false;
}
}
struct agx_oq_heap {
/* The GPU allocation itself */
struct agx_device *dev;
struct agx_bo *bo;
/* Bitset of query indices that are in use */
BITSET_DECLARE(available, AGX_MAX_OCCLUSION_QUERIES);
};
static void
agx_destroy_oq_heap(void *heap_)
{
struct agx_oq_heap *heap = heap_;
agx_bo_unreference(heap->dev, heap->bo);
}
static struct agx_oq_heap *
agx_alloc_oq_heap(struct agx_context *ctx)
{
struct agx_oq_heap *heap = rzalloc(ctx, struct agx_oq_heap);
ralloc_set_destructor(heap, agx_destroy_oq_heap);
heap->dev = agx_device(ctx->base.screen);
heap->bo =
agx_bo_create(heap->dev, AGX_MAX_OCCLUSION_QUERIES * sizeof(uint64_t), 0,
AGX_BO_WRITEBACK, "Occlusion query heap");
/* At the start, everything is available */
BITSET_ONES(heap->available);
return heap;
}
static struct agx_oq_heap *
agx_get_oq_heap(struct agx_context *ctx)
{
if (!ctx->oq)
ctx->oq = agx_alloc_oq_heap(ctx);
return ctx->oq;
}
static struct agx_ptr
agx_alloc_oq(struct agx_context *ctx)
{
struct agx_oq_heap *heap = agx_get_oq_heap(ctx);
/* Find first available */
int ffs = BITSET_FFS(heap->available);
if (!ffs)
return (struct agx_ptr){NULL, 0};
/* Allocate it */
unsigned index = ffs - 1;
BITSET_CLEAR(heap->available, index);
unsigned offset = index * sizeof(uint64_t);
return (struct agx_ptr){
(uint8_t *)agx_bo_map(heap->bo) + offset,
heap->bo->va->addr + offset,
};
}
static unsigned
agx_oq_index(struct agx_context *ctx, struct agx_query *q)
{
assert(is_occlusion(q));
return (q->ptr.gpu - ctx->oq->bo->va->addr) / sizeof(uint64_t);
}
static void
agx_free_oq(struct agx_context *ctx, struct agx_query *q)
{
struct agx_oq_heap *heap = agx_get_oq_heap(ctx);
unsigned index = agx_oq_index(ctx, q);
assert(index < AGX_MAX_OCCLUSION_QUERIES);
assert(!BITSET_TEST(heap->available, index));
BITSET_SET(heap->available, index);
}
uint64_t
agx_get_occlusion_heap(struct agx_batch *batch)
{
if (!batch->ctx->oq)
return 0;
struct agx_bo *bo = batch->ctx->oq->bo;
if (agx_batch_uses_bo(batch, bo))
return bo->va->addr;
else
return 0;
}
static struct pipe_query *
agx_create_query(struct pipe_context *ctx, unsigned query_type, unsigned index)
{
struct agx_query *query = calloc(1, sizeof(struct agx_query));
query->type = query_type;
query->index = index;
/* Set all writer generations to a sentinel that will always compare as
* false, since nothing writes to no queries.
*/
for (unsigned i = 0; i < ARRAY_SIZE(query->writer_generation); ++i) {
query->writer_generation[i] = UINT64_MAX;
}
if (is_occlusion(query)) {
query->ptr = agx_alloc_oq(agx_context(ctx));
} else {
/* TODO: a BO for the query is wasteful, but we benefit from BO list
* tracking / reference counting to deal with lifetimes.
*/
query->bo = agx_bo_create(agx_device(ctx->screen), sizeof(uint64_t) * 2,
0, AGX_BO_WRITEBACK, "Query");
query->ptr = (struct agx_ptr){
.gpu = query->bo->va->addr,
.cpu = agx_bo_map(query->bo),
};
}
if (!query->ptr.gpu) {
free(query);
return NULL;
}
return (struct pipe_query *)query;
}
static void
flush_query_writers(struct agx_context *ctx, struct agx_query *query,
const char *reason)
{
STATIC_ASSERT(ARRAY_SIZE(ctx->batches.generation) == AGX_MAX_BATCHES);
STATIC_ASSERT(ARRAY_SIZE(ctx->batches.slots) == AGX_MAX_BATCHES);
STATIC_ASSERT(ARRAY_SIZE(query->writer_generation) == AGX_MAX_BATCHES);
for (unsigned i = 0; i < AGX_MAX_BATCHES; ++i) {
if (query->writer_generation[i] == ctx->batches.generation[i])
agx_flush_batch_for_reason(ctx, &ctx->batches.slots[i], reason);
}
}
static void
sync_query_writers(struct agx_context *ctx, struct agx_query *query,
const char *reason)
{
for (unsigned i = 0; i < AGX_MAX_BATCHES; ++i) {
if (query->writer_generation[i] == ctx->batches.generation[i])
agx_sync_batch_for_reason(ctx, &ctx->batches.slots[i], reason);
}
}
static bool
is_query_busy(struct agx_context *ctx, struct agx_query *query)
{
for (unsigned i = 0; i < AGX_MAX_BATCHES; ++i) {
if (query->writer_generation[i] == ctx->batches.generation[i])
return true;
}
return false;
}
static void
agx_destroy_query(struct pipe_context *pctx, struct pipe_query *pquery)
{
struct agx_context *ctx = agx_context(pctx);
struct agx_query *query = (struct agx_query *)pquery;
struct agx_device *dev = agx_device(pctx->screen);
/* We don't reference count the occlusion query allocations, so we need to
* sync writers when destroying so we can freely write from the CPU after
* it's destroyed, since the driver will assume an available query is idle.
*
* For other queries, the BO itself is reference counted after the pipe_query
* is destroyed so we don't need to flush.
*/
if (is_occlusion(query)) {
sync_query_writers(ctx, query, "Occlusion query destroy");
agx_free_oq(ctx, query);
} else {
agx_bo_unreference(dev, query->bo);
}
free(pquery);
}
static bool
agx_begin_query(struct pipe_context *pctx, struct pipe_query *pquery)
{
struct agx_context *ctx = agx_context(pctx);
struct agx_query *query = (struct agx_query *)pquery;
ctx->dirty |= AGX_DIRTY_QUERY;
switch (query->type) {
case PIPE_QUERY_OCCLUSION_COUNTER:
case PIPE_QUERY_OCCLUSION_PREDICATE:
case PIPE_QUERY_OCCLUSION_PREDICATE_CONSERVATIVE:
ctx->occlusion_query = query;
break;
case PIPE_QUERY_PRIMITIVES_GENERATED:
ctx->prims_generated[query->index] = query;
break;
case PIPE_QUERY_PRIMITIVES_EMITTED:
ctx->tf_prims_generated[query->index] = query;
break;
case PIPE_QUERY_SO_OVERFLOW_PREDICATE:
ctx->tf_overflow[query->index] = query;
break;
case PIPE_QUERY_SO_OVERFLOW_ANY_PREDICATE:
ctx->tf_any_overflow = query;
break;
case PIPE_QUERY_TIME_ELAPSED:
ctx->time_elapsed = query;
break;
case PIPE_QUERY_TIMESTAMP:
/* No-op */
break;
case PIPE_QUERY_PIPELINE_STATISTICS_SINGLE:
assert(query->index < ARRAY_SIZE(ctx->pipeline_statistics));
ctx->pipeline_statistics[query->index] = query;
break;
default:
return false;
}
/* begin_query zeroes, sync so we can do that write from the CPU */
sync_query_writers(ctx, query, "Query overwritten");
uint64_t *ptr = query->ptr.cpu;
ptr[0] = 0;
if (query->type == PIPE_QUERY_TIME_ELAPSED) {
/* Timestamp begin in second record, the timestamp end in the first */
ptr[1] = UINT64_MAX;
}
return true;
}
static bool
agx_end_query(struct pipe_context *pctx, struct pipe_query *pquery)
{
struct agx_context *ctx = agx_context(pctx);
struct agx_device *dev = agx_device(pctx->screen);
struct agx_query *query = (struct agx_query *)pquery;
ctx->dirty |= AGX_DIRTY_QUERY;
switch (query->type) {
case PIPE_QUERY_OCCLUSION_COUNTER:
case PIPE_QUERY_OCCLUSION_PREDICATE:
case PIPE_QUERY_OCCLUSION_PREDICATE_CONSERVATIVE:
ctx->occlusion_query = NULL;
return true;
case PIPE_QUERY_PRIMITIVES_GENERATED:
ctx->prims_generated[query->index] = NULL;
return true;
case PIPE_QUERY_PRIMITIVES_EMITTED:
ctx->tf_prims_generated[query->index] = NULL;
return true;
case PIPE_QUERY_SO_OVERFLOW_PREDICATE:
ctx->tf_overflow[query->index] = NULL;
return true;
case PIPE_QUERY_SO_OVERFLOW_ANY_PREDICATE:
ctx->tf_any_overflow = NULL;
return true;
case PIPE_QUERY_TIME_ELAPSED:
ctx->time_elapsed = NULL;
return true;
case PIPE_QUERY_PIPELINE_STATISTICS_SINGLE:
assert(query->index < ARRAY_SIZE(ctx->pipeline_statistics));
ctx->pipeline_statistics[query->index] = NULL;
return true;
case PIPE_QUERY_TIMESTAMP: {
/* Timestamp logically written now, set up batches to MAX their finish
* time in. If there are no batches, it's just the current time stamp.
*/
agx_add_timestamp_end_query(ctx, query);
uint64_t *value = query->ptr.cpu;
*value = agx_get_gpu_timestamp(dev);
return true;
}
default:
return false;
}
}
enum query_copy_type {
QUERY_COPY_NORMAL,
QUERY_COPY_BOOL32,
QUERY_COPY_BOOL64,
QUERY_COPY_TIMESTAMP,
QUERY_COPY_TIME_ELAPSED,
};
static enum query_copy_type
classify_query_type(enum pipe_query_type type)
{
switch (type) {
case PIPE_QUERY_OCCLUSION_PREDICATE:
case PIPE_QUERY_OCCLUSION_PREDICATE_CONSERVATIVE:
return QUERY_COPY_BOOL32;
case PIPE_QUERY_SO_OVERFLOW_PREDICATE:
case PIPE_QUERY_SO_OVERFLOW_ANY_PREDICATE:
return QUERY_COPY_BOOL64;
case PIPE_QUERY_TIMESTAMP:
return QUERY_COPY_TIMESTAMP;
case PIPE_QUERY_TIME_ELAPSED:
return QUERY_COPY_TIME_ELAPSED;
default:
return QUERY_COPY_NORMAL;
}
}
static bool
agx_get_query_result(struct pipe_context *pctx, struct pipe_query *pquery,
bool wait, union pipe_query_result *vresult)
{
struct agx_query *query = (struct agx_query *)pquery;
struct agx_context *ctx = agx_context(pctx);
struct agx_device *dev = agx_device(pctx->screen);
/* TODO: Honour `wait` */
sync_query_writers(ctx, query, "Reading query results");
uint64_t *ptr = query->ptr.cpu;
uint64_t value = *ptr;
switch (classify_query_type(query->type)) {
case QUERY_COPY_BOOL32:
vresult->u64 = ((uint32_t)value) != 0;
return true;
case QUERY_COPY_BOOL64:
vresult->u64 = value != 0;
return true;
case QUERY_COPY_NORMAL:
vresult->u64 = value;
return true;
case QUERY_COPY_TIMESTAMP:
vresult->u64 = agx_gpu_timestamp_to_ns(dev, value);
return true;
case QUERY_COPY_TIME_ELAPSED:
/* end - begin */
vresult->u64 = agx_gpu_timestamp_to_ns(dev, ptr[0] - ptr[1]);
return true;
default:
UNREACHABLE("Other queries not yet supported");
}
}
static unsigned
result_type_size(enum pipe_query_value_type result_type)
{
return (result_type <= PIPE_QUERY_TYPE_U32) ? 4 : 8;
}
static void
agx_get_query_result_resource_cpu(struct agx_context *ctx,
struct agx_query *query,
enum pipe_query_flags flags,
enum pipe_query_value_type result_type,
int index, struct pipe_resource *resource,
unsigned offset)
{
union pipe_query_result result;
if (index < 0) {
/* availability */
result.u64 = !is_query_busy(ctx, query);
} else {
bool ready =
agx_get_query_result(&ctx->base, (void *)query, true, &result);
assert(ready);
}
/* Clamp to type, arb_query_buffer_object-qbo tests */
if (result_type == PIPE_QUERY_TYPE_U32) {
result.u32 = MIN2(result.u64, u_uintN_max(32));
} else if (result_type == PIPE_QUERY_TYPE_I32) {
int64_t x = result.u64;
x = MAX2(MIN2(x, u_intN_max(32)), u_intN_min(32));
result.u32 = x;
}
pipe_buffer_write(&ctx->base, resource, offset,
result_type_size(result_type), &result.u64);
}
static bool
agx_get_query_result_resource_gpu(struct agx_context *ctx,
struct agx_query *query,
enum pipe_query_flags flags,
enum pipe_query_value_type result_type,
int index, struct pipe_resource *prsrc,
unsigned offset)
{
/* Handle availability queries on CPU */
if (index < 0)
return false;
/* TODO: timer queries on GPU */
if (query->type == PIPE_QUERY_TIMESTAMP ||
query->type == PIPE_QUERY_TIME_ELAPSED)
return false;
flush_query_writers(ctx, query, util_str_query_type(query->type, true));
struct agx_resource *rsrc = agx_resource(prsrc);
enum query_copy_type copy_type = classify_query_type(query->type);
struct agx_batch *batch = agx_get_compute_batch(ctx);
agx_batch_init_state(batch);
agx_dirty_all(ctx);
/* Set params */
agx_batch_writes_range(batch, rsrc, offset, result_type_size(result_type));
unsigned bool_size = copy_type == QUERY_COPY_BOOL64 ? 8
: copy_type == QUERY_COPY_BOOL32 ? 4
: 0;
libagx_copy_query_gl(batch, agx_1d(1), AGX_BARRIER_ALL, query->ptr.gpu,
agx_map_gpu(rsrc) + offset, result_type, bool_size);
return true;
}
static void
agx_get_query_result_resource(struct pipe_context *pipe, struct pipe_query *q,
enum pipe_query_flags flags,
enum pipe_query_value_type result_type, int index,
struct pipe_resource *resource, unsigned offset)
{
struct agx_query *query = (struct agx_query *)q;
struct agx_context *ctx = agx_context(pipe);
/* Try to copy on the GPU */
if (!agx_get_query_result_resource_gpu(ctx, query, flags, result_type, index,
resource, offset)) {
/* Else, fallback to CPU */
agx_get_query_result_resource_cpu(ctx, query, flags, result_type, index,
resource, offset);
}
}
static void
agx_set_active_query_state(struct pipe_context *pipe, bool enable)
{
struct agx_context *ctx = agx_context(pipe);
ctx->active_queries = enable;
ctx->dirty |= AGX_DIRTY_QUERY;
}
static void
agx_add_query_to_batch(struct agx_batch *batch, struct agx_query *query)
{
unsigned idx = agx_batch_idx(batch);
struct agx_bo *bo = is_occlusion(query) ? batch->ctx->oq->bo : query->bo;
agx_batch_add_bo(batch, bo);
query->writer_generation[idx] = batch->ctx->batches.generation[idx];
}
void
agx_batch_add_timestamp_query(struct agx_batch *batch, struct agx_query *q)
{
if (q) {
agx_add_query_to_batch(batch, q);
util_dynarray_append(&batch->timestamps, q->ptr);
}
}
uint16_t
agx_get_oq_index(struct agx_batch *batch, struct agx_query *query)
{
agx_add_query_to_batch(batch, query);
return agx_oq_index(batch->ctx, query);
}
uint64_t
agx_get_query_address(struct agx_batch *batch, struct agx_query *query)
{
if (query) {
agx_add_query_to_batch(batch, query);
return query->ptr.gpu;
} else {
return AGX_SCRATCH_PAGE_ADDRESS;
}
}
void
agx_finish_batch_queries(struct agx_batch *batch, uint64_t begin_ts,
uint64_t end_ts)
{
/* Remove the batch as write from all queries by incrementing the generation
* of the batch.
*/
batch->ctx->batches.generation[agx_batch_idx(batch)]++;
/* Write out timestamps */
util_dynarray_foreach(&batch->timestamps, struct agx_ptr, it) {
uint64_t *ptr = it->cpu;
ptr[0] = MAX2(ptr[0], end_ts);
ptr[1] = MIN2(ptr[1], begin_ts);
}
}
void
agx_query_increment_cpu(struct agx_context *ctx, struct agx_query *query,
uint64_t increment)
{
if (!query)
return;
sync_query_writers(ctx, query, "CPU query increment");
uint64_t *value = query->ptr.cpu;
*value += increment;
}
static void
agx_render_condition(struct pipe_context *pipe, struct pipe_query *query,
bool condition, enum pipe_render_cond_flag mode)
{
struct agx_context *ctx = agx_context(pipe);
ctx->cond_query = query;
ctx->cond_cond = condition;
ctx->cond_mode = mode;
}
bool
agx_render_condition_check_inner(struct agx_context *ctx)
{
assert(ctx->cond_query != NULL && "precondition");
perf_debug_ctx(ctx, "Implementing conditional rendering on the CPU");
union pipe_query_result res = {0};
bool wait = ctx->cond_mode != PIPE_RENDER_COND_NO_WAIT &&
ctx->cond_mode != PIPE_RENDER_COND_BY_REGION_NO_WAIT;
struct pipe_query *pq = (struct pipe_query *)ctx->cond_query;
if (agx_get_query_result(&ctx->base, pq, wait, &res))
return res.u64 != ctx->cond_cond;
return true;
}
void
agx_init_query_functions(struct pipe_context *pctx)
{
pctx->create_query = agx_create_query;
pctx->destroy_query = agx_destroy_query;
pctx->begin_query = agx_begin_query;
pctx->end_query = agx_end_query;
pctx->get_query_result = agx_get_query_result;
pctx->get_query_result_resource = agx_get_query_result_resource;
pctx->set_active_query_state = agx_set_active_query_state;
pctx->render_condition = agx_render_condition;
/* By default queries are active */
agx_context(pctx)->active_queries = true;
}