Skip to content

Commit 2117cd2

Browse files
jdolanclaude
andcommitted
Merge branch 'occlusion-query'
Adds occlusion query support (QueryPool, RenderPass::beginQuery/ endQuery, CopyPass::downloadQueryResults), guarded so it's a no-op when the linked SDL3 doesn't provide the underlying prototype API. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2 parents 9c25692 + 5da5516 commit 2117cd2

11 files changed

Lines changed: 458 additions & 0 deletions

File tree

Examples/Hello.c

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ typedef struct {
9393
* @brief The @c Scene.
9494
*/
9595
Scene scene;
96+
97+
/**
98+
* @brief Occlusion query proof-of-concept: pool, readback buffer, and frame counter.
99+
*/
100+
QueryPool *occlusionQueryPool;
101+
SDL_GPUTransferBuffer *occlusionQueryTransfer;
102+
Uint32 frames;
96103
} AppState;
97104

98105
static AppState application;
@@ -213,10 +220,39 @@ static void drawScene(AppState *app, CommandBuffer *commands) {
213220
RenderPass *pass = $(commands, beginRenderPass, &color, 1, &depth);
214221
$(pass, bindPipeline, scene->pipeline);
215222
$(pass, bindVertexBuffers, 0, &(SDL_GPUBufferBinding) { .buffer = scene->vertexBuffer->buffer }, 1);
223+
$(pass, beginQuery, app->occlusionQueryPool, 0);
216224
$(pass, drawPrimitives, (Uint32) SDL_arraysize(vertices), 1, 0, 0);
225+
$(pass, endQuery, app->occlusionQueryPool, 0);
217226
release(pass);
218227
}
219228

229+
/**
230+
* @brief Occlusion query proof-of-concept: downloads and logs this frame's result.
231+
* @details A real application would defer the readback to a later frame instead
232+
* of blocking on the GPU like this. When occlusion queries are unsupported by
233+
* the linked SDL3, this always reports a "not occluded" sentinel value.
234+
*/
235+
static void logOcclusionQueryResult(AppState *app) {
236+
237+
CommandBuffer *commands = $(app->renderDevice, acquireCommandBuffer);
238+
239+
CopyPass *copyPass = $(commands, beginCopyPass);
240+
$(copyPass, downloadQueryResults, app->occlusionQueryPool, 0, 1, &(SDL_GPUTransferBufferLocation) {
241+
.transfer_buffer = app->occlusionQueryTransfer,
242+
});
243+
release(copyPass);
244+
245+
SDL_GPUFence *fence = $(commands, submitAndFence);
246+
release(commands);
247+
248+
$(app->renderDevice, waitForFences, true, &fence, 1);
249+
$(app->renderDevice, releaseFence, fence);
250+
251+
const Uint64 *result = $(app->renderDevice, mapTransferBuffer, app->occlusionQueryTransfer, false);
252+
SDL_Log("Occlusion query result: %" SDL_PRIu64 " samples passed", *result);
253+
$(app->renderDevice, unmapTransferBuffer, app->occlusionQueryTransfer);
254+
}
255+
220256
#pragma mark - SDL application callbacks
221257

222258
/**
@@ -254,6 +290,16 @@ SDL_AppResult SDL_AppInit(void **appState, int argc, char *argv[]) {
254290

255291
initScene(app);
256292

293+
app->occlusionQueryPool = $(app->renderDevice, createQueryPool, &(SDL_GPUQueryPoolCreateInfo) {
294+
.type = SDL_GPU_QUERY_PRECISE_OCCLUSION,
295+
.query_count = 1,
296+
});
297+
298+
app->occlusionQueryTransfer = $(app->renderDevice, createTransferBuffer, &(SDL_GPUTransferBufferCreateInfo) {
299+
.usage = SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD,
300+
.size = sizeof(Uint64),
301+
});
302+
257303
return SDL_APP_CONTINUE;
258304
}
259305

@@ -268,6 +314,10 @@ SDL_AppResult SDL_AppIterate(void *appState) {
268314
if (commands) {
269315
drawScene(app, commands);
270316
$(app->renderDevice, endFrame);
317+
318+
if ((app->frames++ % 60) == 0) {
319+
logOcclusionQueryResult(app);
320+
}
271321
}
272322

273323
return SDL_APP_CONTINUE;
@@ -298,6 +348,9 @@ void SDL_AppQuit(void *appState, SDL_AppResult result) {
298348
release(app->scene.pipeline);
299349
release(app->scene.vertexBuffer);
300350

351+
release(app->occlusionQueryPool);
352+
$(app->renderDevice, releaseTransferBuffer, app->occlusionQueryTransfer);
353+
301354
release(app->framebuffer);
302355
release(app->renderDevice);
303356

ObjectivelyGPU.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
CEBA3F772FF3418E00758CA6 /* Objectively.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEA0030000000000000000F0 /* Objectively.framework */; };
5656
CEBA3F782FF3419100758CA6 /* ObjectivelyGPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6251F22589273797656C269E /* ObjectivelyGPU.framework */; };
5757
CECF83982FEF6A7200CB0CCA /* Mathlib.h in Headers */ = {isa = PBXBuildFile; fileRef = CECF83972FEF6A7200CB0CCA /* Mathlib.h */; settings = {ATTRIBUTES = (Public, ); }; };
58+
496C55EEAC4C86E3B7C6550E /* QueryPool.h in Headers */ = {isa = PBXBuildFile; fileRef = 989E64805790578A941CB5B9 /* QueryPool.h */; settings = {ATTRIBUTES = (Public, ); }; };
59+
45AD38AB8109398E51564CFD /* QueryPool.c in Sources */ = {isa = PBXBuildFile; fileRef = 872CC6C6CBA08F1A36626F57 /* QueryPool.c */; };
5860
CECF83B62FEF765400CB0CCA /* Hello.frag.metal in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECF83A52FEF712F00CB0CCA /* Hello.frag.metal */; };
5961
CECF83B72FEF765400CB0CCA /* Hello.frag.spv in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECF83A62FEF712F00CB0CCA /* Hello.frag.spv */; };
6062
CECF83B92FEF765400CB0CCA /* Hello.vert.metal in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECF83A82FEF712F00CB0CCA /* Hello.vert.metal */; };
@@ -182,6 +184,8 @@
182184
CECF83952FEF65FF00CB0CCA /* Makefile.am */ = {isa = PBXFileReference; lastKnownFileType = text; path = Makefile.am; sourceTree = "<group>"; };
183185
CECF83962FEF65FF00CB0CCA /* ObjectivelyGPU.pc.in */ = {isa = PBXFileReference; lastKnownFileType = text; path = ObjectivelyGPU.pc.in; sourceTree = "<group>"; };
184186
CECF83972FEF6A7200CB0CCA /* Mathlib.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Mathlib.h; sourceTree = "<group>"; };
187+
989E64805790578A941CB5B9 /* QueryPool.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QueryPool.h; sourceTree = "<group>"; };
188+
872CC6C6CBA08F1A36626F57 /* QueryPool.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = QueryPool.c; sourceTree = "<group>"; };
185189
CECF83A32FEF6F5B00CB0CCA /* Makefile.am */ = {isa = PBXFileReference; lastKnownFileType = text; path = Makefile.am; sourceTree = "<group>"; };
186190
CECF83A42FEF712F00CB0CCA /* Hello.frag.glsl */ = {isa = PBXFileReference; lastKnownFileType = text; path = Hello.frag.glsl; sourceTree = "<group>"; };
187191
CECF83A52FEF712F00CB0CCA /* Hello.frag.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Hello.frag.metal; sourceTree = "<group>"; };
@@ -306,6 +310,8 @@
306310
CEBA3F652FF3211F00758CA6 /* GraphicsPipeline.h */,
307311
CEBA3F662FF3211F00758CA6 /* GraphicsPipeline.c */,
308312
CECF83972FEF6A7200CB0CCA /* Mathlib.h */,
313+
989E64805790578A941CB5B9 /* QueryPool.h */,
314+
872CC6C6CBA08F1A36626F57 /* QueryPool.c */,
309315
9220C57BBAA3C951D8083799 /* RenderDevice.h */,
310316
77315F9BC357C97B885E883C /* RenderDevice.c */,
311317
206D63692B601BD90F377BF5 /* RenderPass.h */,
@@ -397,6 +403,7 @@
397403
CECF83C72FF03A3500CB0CCA /* Framebuffer.h in Headers */,
398404
CEBA3F6B2FF3211F00758CA6 /* GraphicsPipeline.h in Headers */,
399405
CECF83982FEF6A7200CB0CCA /* Mathlib.h in Headers */,
406+
496C55EEAC4C86E3B7C6550E /* QueryPool.h in Headers */,
400407
A09AD953AC672E0DE9D1E26D /* ObjectivelyGPU.h in Headers */,
401408
59897FC7BCC194032E971A9B /* RenderDevice.h in Headers */,
402409
683F683E23149C267D613EA0 /* RenderPass.h in Headers */,
@@ -579,6 +586,7 @@
579586
CCCB11DB70E4A962360A2119 /* CopyPass.c in Sources */,
580587
CECF83C82FF03A3500CB0CCA /* Framebuffer.c in Sources */,
581588
CEBA3F702FF3211F00758CA6 /* GraphicsPipeline.c in Sources */,
589+
45AD38AB8109398E51564CFD /* QueryPool.c in Sources */,
582590
CEBA3F732FF3211F00758CA6 /* Sampler.c in Sources */,
583591
CEBA3F722FF3211F00758CA6 /* Shader.c in Sources */,
584592
86E5132F5DCCB6B674151343 /* RenderDevice.c in Sources */,

Sources/ObjectivelyGPU/CopyPass.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
#include "CommandBuffer.h"
2828
#include "CopyPass.h"
29+
#include "QueryPool.h"
2930
#include "RenderDevice.h"
3031

3132
#define _Class _CopyPass
@@ -83,6 +84,32 @@ static void downloadTexture(const CopyPass *self, const SDL_GPUTextureRegion *sr
8384
SDL_DownloadFromGPUTexture(self->pass, src, dst);
8485
}
8586

87+
/**
88+
* @fn void CopyPass::downloadQueryResults(const CopyPass *self, QueryPool *pool, Uint32 firstQuery, Uint32 count, const SDL_GPUTransferBufferLocation *dst)
89+
* @memberof CopyPass
90+
*/
91+
static void downloadQueryResults(const CopyPass *self, QueryPool *pool, Uint32 firstQuery, Uint32 count, const SDL_GPUTransferBufferLocation *dst) {
92+
93+
assert(pool);
94+
95+
#ifdef SDL_GPU_OCCLUSION_QUERY
96+
if (pool->pool) {
97+
SDL_DownloadGPUQueryResults(self->pass, pool->pool, firstQuery, count, (SDL_GPUTransferBufferLocation *) dst);
98+
return;
99+
}
100+
#endif
101+
102+
// Occlusion queries are unsupported: no GPU work will ever populate `dst`, so
103+
// write a "not occluded" sentinel for every requested result directly, rather
104+
// than leaving the destination uninitialized (which callers would likely
105+
// misread as "occluded", incorrectly culling everything).
106+
Uint64 *results = $(self->commands->device, mapTransferBuffer, dst->transfer_buffer, false);
107+
for (Uint32 i = 0; i < count; i++) {
108+
results[(dst->offset / sizeof(Uint64)) + i] = 1;
109+
}
110+
$(self->commands->device, unmapTransferBuffer, dst->transfer_buffer);
111+
}
112+
86113
/**
87114
* @fn CopyPass *CopyPass::init(CopyPass *self, CommandBuffer *commands, SDL_GPUCopyPass *pass)
88115
* @memberof CopyPass
@@ -159,6 +186,7 @@ static void initialize(Class *clazz) {
159186
((CopyPassInterface *) clazz->interface)->copyTextureToTexture = copyTextureToTexture;
160187
((CopyPassInterface *) clazz->interface)->downloadBuffer = downloadBuffer;
161188
((CopyPassInterface *) clazz->interface)->downloadTexture = downloadTexture;
189+
((CopyPassInterface *) clazz->interface)->downloadQueryResults = downloadQueryResults;
162190
((CopyPassInterface *) clazz->interface)->init = init;
163191
((CopyPassInterface *) clazz->interface)->uploadBuffer = uploadBuffer;
164192
((CopyPassInterface *) clazz->interface)->uploadData = uploadData;

Sources/ObjectivelyGPU/CopyPass.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
typedef struct CommandBuffer CommandBuffer;
3636
typedef struct CopyPass CopyPass;
3737
typedef struct CopyPassInterface CopyPassInterface;
38+
typedef struct QueryPool QueryPool;
3839

3940
/**
4041
* @brief A scoped copy pass for uploading CPU data and copying GPU resources.
@@ -121,6 +122,17 @@ struct CopyPassInterface {
121122
*/
122123
void (*downloadTexture)(const CopyPass *self, const SDL_GPUTextureRegion *src, const SDL_GPUTextureTransferInfo *dst);
123124

125+
/**
126+
* @fn void CopyPass::downloadQueryResults(const CopyPass *self, QueryPool *pool, Uint32 firstQuery, Uint32 count, const SDL_GPUTransferBufferLocation *dst)
127+
* @brief Downloads a range of query results to a CPU-accessible transfer buffer.
128+
* @details Each result is a `Uint64`, laid out contiguously starting at @p dst.
129+
* When @p pool is backed by an unsupported SDL3 build, every result is
130+
* reported as "not occluded" (nonzero) rather than left uninitialized; see
131+
* `QueryPool`.
132+
* @memberof CopyPass
133+
*/
134+
void (*downloadQueryResults)(const CopyPass *self, QueryPool *pool, Uint32 firstQuery, Uint32 count, const SDL_GPUTransferBufferLocation *dst);
135+
124136
/**
125137
* @fn CopyPass *CopyPass::init(CopyPass *self, CommandBuffer *commands, SDL_GPUCopyPass *pass)
126138
* @brief Initializes this CopyPass wrapping the given SDL copy pass.

Sources/ObjectivelyGPU/Makefile.am

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pkginclude_HEADERS = \
1515
Framebuffer.h \
1616
GraphicsPipeline.h \
1717
Mathlib.h \
18+
QueryPool.h \
1819
RenderDevice.h \
1920
RenderPass.h \
2021
Sampler.h \
@@ -32,6 +33,7 @@ libObjectivelyGPU_la_SOURCES = \
3233
CopyPass.c \
3334
Framebuffer.c \
3435
GraphicsPipeline.c \
36+
QueryPool.c \
3537
RenderDevice.c \
3638
RenderPass.c \
3739
Sampler.c \

Sources/ObjectivelyGPU/QueryPool.c

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* ObjectivelyGPU: Object oriented graphics framework for SDL3 and C.
3+
* Copyright (C) 2026 Jay Dolan <jay@jaydolan.com>
4+
*
5+
* This software is provided 'as-is', without any express or implied
6+
* warranty. In no event will the authors be held liable for any damages
7+
* arising from the use of this software.
8+
*
9+
* Permission is granted to anyone to use this software for any purpose,
10+
* including commercial applications, and to alter it and redistribute it
11+
* freely, subject to the following restrictions:
12+
*
13+
* 1. The origin of this software must not be misrepresented; you must not
14+
* claim that you wrote the original software. If you use this software
15+
* in a product, an acknowledgment in the product documentation would be
16+
* appreciated but is not required.
17+
*
18+
* 2. Altered source versions must be plainly marked as such, and must not be
19+
* misrepresented as being the original software.
20+
*
21+
* 3. This notice may not be removed or altered from any source distribution.
22+
*/
23+
24+
#include <assert.h>
25+
26+
#include "QueryPool.h"
27+
#include "RenderDevice.h"
28+
29+
#define _Class _QueryPool
30+
31+
#pragma mark - Object
32+
33+
/**
34+
* @see Object::dealloc(Object *)
35+
*/
36+
static void dealloc(Object *self) {
37+
38+
QueryPool *this = (QueryPool *) self;
39+
40+
if (this->device) {
41+
#ifdef SDL_GPU_OCCLUSION_QUERY
42+
if (this->pool) {
43+
SDL_ReleaseGPUQueryPool(this->device->device, this->pool);
44+
}
45+
#endif
46+
release(this->device);
47+
}
48+
49+
super(Object, self, dealloc);
50+
}
51+
52+
#pragma mark - QueryPool
53+
54+
/**
55+
* @fn QueryPool *QueryPool::initWithDevice(QueryPool *self, RenderDevice *device, const SDL_GPUQueryPoolCreateInfo *info)
56+
* @memberof QueryPool
57+
*/
58+
static QueryPool *initWithDevice(QueryPool *self, RenderDevice *device, const SDL_GPUQueryPoolCreateInfo *info) {
59+
60+
assert(device);
61+
assert(info);
62+
63+
self = (QueryPool *) super(Object, self, init);
64+
if (self) {
65+
66+
self->device = retain(device);
67+
self->type = info->type;
68+
self->queryCount = info->query_count;
69+
70+
#ifdef SDL_GPU_OCCLUSION_QUERY
71+
self->pool = SDL_CreateGPUQueryPool(device->device, (SDL_GPUQueryPoolCreateInfo *) info);
72+
GPU_Assert(self->pool, "SDL_CreateGPUQueryPool");
73+
#else
74+
self->pool = NULL; // Occlusion queries are not supported by this SDL_gpu build.
75+
#endif
76+
}
77+
78+
return self;
79+
}
80+
81+
#pragma mark - Class lifecycle
82+
83+
/**
84+
* @see Class::initialize(Class *)
85+
*/
86+
static void initialize(Class *clazz) {
87+
88+
((ObjectInterface *) clazz->interface)->dealloc = dealloc;
89+
90+
((QueryPoolInterface *) clazz->interface)->initWithDevice = initWithDevice;
91+
}
92+
93+
/**
94+
* @fn Class *QueryPool::_QueryPool(void)
95+
* @memberof QueryPool
96+
*/
97+
Class *_QueryPool(void) {
98+
static Class *clazz;
99+
static Once once;
100+
101+
do_once(&once, {
102+
clazz = _initialize(&(const ClassDef) {
103+
.name = "QueryPool",
104+
.superclass = _Object(),
105+
.instanceSize = sizeof(QueryPool),
106+
.interfaceOffset = offsetof(QueryPool, interface),
107+
.interfaceSize = sizeof(QueryPoolInterface),
108+
.initialize = initialize,
109+
});
110+
});
111+
112+
return clazz;
113+
}
114+
115+
#undef _Class

0 commit comments

Comments
 (0)