/*
* wgpu-native v29.0.1.1 — encoder-level wgpuCommandEncoderWriteTimestamp
* makes the submission never complete (Metal backend).
*
* Environment this was captured on:
* macOS 26.5.2 (build 25F84), Apple M2, Metal backend,
* wgpu-native v29.0.1.1 (wgpu-macos-aarch64-release prebuilt).
*
* Sequence:
* - request an adapter/device with the "timestamp-query" feature and the
* native TimestampQueryInsideEncoders feature (both advertised by the
* adapter, both accepted by requestDevice),
* - create a 2-entry timestamp query set,
* - on a command encoder, outside of any pass:
* writeTimestamp(0) -> copyBufferToBuffer -> writeTimestamp(1),
* - resolveQuerySet into a buffer, copy it to a mappable buffer, finish,
* - submit and wait for that submission with wgpuDevicePoll(wait = true).
*
* Expected: the submission completes and the two queries resolve to nonzero,
* nondecreasing timestamps.
* Actual: the submission never completes. wgpuDevicePoll(wait = true) with
* the submission index blocks indefinitely and no validation error
* or device-lost callback is reported. A 10 s watchdog in this
* program prints a diagnostic and exits nonzero instead of hanging.
*
* The encoder-level timestamp writes are the whole ingredient:
* --no-write-timestamp keeps the copy, the resolve and the readback and
* drops only the two writeTimestamp calls: the
* submission then completes normally.
* --minimal submits an encoder holding ONE writeTimestamp and
* nothing else — no copy, no resolveQuerySet, no
* readback: that alone wedges the submission too.
*
* The wedge is not an artifact of the wgpu-native submit/poll extras used
* here: replacing wgpuQueueSubmitForIndex + wgpuDevicePoll(wait, &index) with
* plain wgpuQueueSubmit + wgpuDevicePoll(wait, NULL) blocks identically.
*
* Build (any stock wgpu-native install; WGPU is its root directory):
* cc -std=c11 -O2 main.c -o repro -I"$WGPU/include" \
* "$WGPU/lib/libwgpu_native.dylib" -rpath "$WGPU/lib"
*
* Run:
* ./repro # reproduces the wedge (exit code 1)
* ./repro --minimal # smallest wedging encoder
* ./repro --no-write-timestamp # control: completes normally
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <webgpu/webgpu.h>
#include <webgpu/wgpu.h>
#define WATCHDOG_SECONDS 10
/* wgpu-native's native feature for timestamp writes outside of a pass. */
#define FEATURE_TIMESTAMP_QUERY_INSIDE_ENCODERS \
((WGPUFeatureName) WGPUNativeFeature_TimestampQueryInsideEncoders)
static const char kWatchdogMessage[] =
"FAIL: submit did not complete within 10s (queue wedged)\n";
static void on_watchdog(int signum)
{
(void) signum;
/* Async-signal-safe: write(2) and _exit(2) only. */
ssize_t ignored = write(STDOUT_FILENO, kWatchdogMessage,
sizeof(kWatchdogMessage) - 1);
(void) ignored;
_exit(1);
}
static void arm_watchdog(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = on_watchdog;
sigaction(SIGALRM, &sa, NULL);
alarm(WATCHDOG_SECONDS);
}
static void disarm_watchdog(void)
{
alarm(0);
}
static void die(const char* what)
{
printf("FAIL: %s\n", what);
exit(2);
}
static WGPUStringView view(const char* text)
{
WGPUStringView v;
v.data = text;
v.length = WGPU_STRLEN;
return v;
}
static void print_view(const char* label, WGPUStringView v)
{
printf("%s%.*s\n", label, (int) (v.data ? v.length : 0),
v.data ? v.data : "");
}
static void on_uncaptured_error(WGPUDevice const* device, WGPUErrorType type,
WGPUStringView message, void* u1, void* u2)
{
(void) device;
(void) u1;
(void) u2;
printf("uncaptured error (type 0x%08x): %.*s\n", (unsigned) type,
(int) (message.data ? message.length : 0),
message.data ? message.data : "");
}
static void on_device_lost(WGPUDevice const* device, WGPUDeviceLostReason reason,
WGPUStringView message, void* u1, void* u2)
{
(void) device;
(void) u1;
(void) u2;
printf("device lost (reason 0x%08x): %.*s\n", (unsigned) reason,
(int) (message.data ? message.length : 0),
message.data ? message.data : "");
}
static void on_adapter(WGPURequestAdapterStatus status, WGPUAdapter adapter,
WGPUStringView message, void* userdata1, void* userdata2)
{
(void) userdata2;
if (status != WGPURequestAdapterStatus_Success)
printf("requestAdapter failed (status 0x%08x): %.*s\n",
(unsigned) status, (int) (message.data ? message.length : 0),
message.data ? message.data : "");
*(WGPUAdapter*) userdata1 = adapter;
}
static void on_device(WGPURequestDeviceStatus status, WGPUDevice device,
WGPUStringView message, void* userdata1, void* userdata2)
{
(void) userdata2;
if (status != WGPURequestDeviceStatus_Success)
printf("requestDevice failed (status 0x%08x): %.*s\n",
(unsigned) status, (int) (message.data ? message.length : 0),
message.data ? message.data : "");
*(WGPUDevice*) userdata1 = device;
}
static void on_map(WGPUMapAsyncStatus status, WGPUStringView message,
void* userdata1, void* userdata2)
{
(void) userdata2;
if (status != WGPUMapAsyncStatus_Success)
printf("mapAsync failed (status 0x%08x): %.*s\n", (unsigned) status,
(int) (message.data ? message.length : 0),
message.data ? message.data : "");
*(int*) userdata1 = 1;
}
static WGPUBuffer make_buffer(WGPUDevice device, const char* label,
uint64_t size, WGPUBufferUsage usage)
{
WGPUBufferDescriptor desc;
memset(&desc, 0, sizeof(desc));
desc.label = view(label);
desc.usage = usage;
desc.size = size;
WGPUBuffer buffer = wgpuDeviceCreateBuffer(device, &desc);
if (!buffer)
die("createBuffer returned NULL");
return buffer;
}
int main(int argc, char** argv)
{
/* Number of encoder-level writeTimestamp calls, and whether the encoder
carries anything else at all. Both are only there to show which
ingredient wedges the submission. */
int timestampCount = 2;
int minimal = 0;
for (int i = 1; i < argc; ++i)
{
if (strcmp(argv[i], "--no-write-timestamp") == 0)
timestampCount = 0;
else if (strcmp(argv[i], "--minimal") == 0)
{
timestampCount = 1;
minimal = 1;
}
}
printf("encoder-level writeTimestamp calls: %d\n", timestampCount);
printf("other commands in the encoder: %s\n",
minimal ? "none" : "copy, resolveQuerySet, copy");
WGPUInstance instance = wgpuCreateInstance(NULL);
if (!instance)
die("createInstance returned NULL");
WGPURequestAdapterOptions adapterOptions;
memset(&adapterOptions, 0, sizeof(adapterOptions));
adapterOptions.powerPreference = WGPUPowerPreference_HighPerformance;
WGPUAdapter adapter = NULL;
WGPURequestAdapterCallbackInfo adapterCallback;
memset(&adapterCallback, 0, sizeof(adapterCallback));
adapterCallback.mode = WGPUCallbackMode_AllowProcessEvents;
adapterCallback.callback = on_adapter;
adapterCallback.userdata1 = &adapter;
wgpuInstanceRequestAdapter(instance, &adapterOptions, adapterCallback);
for (int spins = 0; !adapter && spins < 1000; ++spins)
wgpuInstanceProcessEvents(instance);
if (!adapter)
die("no adapter");
WGPUAdapterInfo info;
memset(&info, 0, sizeof(info));
if (wgpuAdapterGetInfo(adapter, &info) == WGPUStatus_Success)
{
print_view("adapter: ", info.device);
printf("backend type: 0x%08x (5 = Metal)\n", (unsigned) info.backendType);
}
int hasTimestamp = wgpuAdapterHasFeature(adapter, WGPUFeatureName_TimestampQuery) != 0;
int hasInEncoder = wgpuAdapterHasFeature(adapter, FEATURE_TIMESTAMP_QUERY_INSIDE_ENCODERS) != 0;
printf("adapter advertises timestamp-query: %s\n",
hasTimestamp ? "yes" : "no");
printf("adapter advertises TimestampQueryInsideEncoders: %s\n",
hasInEncoder ? "yes" : "no");
if (!hasTimestamp || !hasInEncoder)
die("adapter does not advertise the required timestamp features");
WGPUFeatureName required[2];
required[0] = WGPUFeatureName_TimestampQuery;
required[1] = FEATURE_TIMESTAMP_QUERY_INSIDE_ENCODERS;
WGPUDeviceDescriptor deviceDesc;
memset(&deviceDesc, 0, sizeof(deviceDesc));
deviceDesc.label = view("repro device");
deviceDesc.requiredFeatureCount = 2;
deviceDesc.requiredFeatures = required;
deviceDesc.uncapturedErrorCallbackInfo.callback = on_uncaptured_error;
deviceDesc.deviceLostCallbackInfo.mode = WGPUCallbackMode_AllowProcessEvents;
deviceDesc.deviceLostCallbackInfo.callback = on_device_lost;
WGPUDevice device = NULL;
WGPURequestDeviceCallbackInfo deviceCallback;
memset(&deviceCallback, 0, sizeof(deviceCallback));
deviceCallback.mode = WGPUCallbackMode_AllowProcessEvents;
deviceCallback.callback = on_device;
deviceCallback.userdata1 = &device;
wgpuAdapterRequestDevice(adapter, &deviceDesc, deviceCallback);
for (int spins = 0; !device && spins < 1000; ++spins)
wgpuInstanceProcessEvents(instance);
if (!device)
die("no device");
WGPUQueue queue = wgpuDeviceGetQueue(device);
printf("timestamp period: %g ns/tick\n",
(double) wgpuQueueGetTimestampPeriod(queue));
WGPUQuerySetDescriptor querySetDesc;
memset(&querySetDesc, 0, sizeof(querySetDesc));
querySetDesc.label = view("timestamps");
querySetDesc.type = WGPUQueryType_Timestamp;
querySetDesc.count = 2;
WGPUQuerySet querySet = wgpuDeviceCreateQuerySet(device, &querySetDesc);
if (!querySet)
die("createQuerySet returned NULL");
const uint64_t kPayloadBytes = 256;
const uint64_t kResultBytes = 2 * sizeof(uint64_t);
WGPUBuffer source = make_buffer(device, "copy source", kPayloadBytes,
WGPUBufferUsage_CopySrc | WGPUBufferUsage_CopyDst);
WGPUBuffer sink = make_buffer(device, "copy sink", kPayloadBytes,
WGPUBufferUsage_CopyDst);
WGPUBuffer resolved = make_buffer(device, "resolved queries", kResultBytes,
WGPUBufferUsage_QueryResolve | WGPUBufferUsage_CopySrc);
WGPUBuffer readback = make_buffer(device, "readback", kResultBytes,
WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead);
WGPUCommandEncoderDescriptor encoderDesc;
memset(&encoderDesc, 0, sizeof(encoderDesc));
encoderDesc.label = view("repro encoder");
WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, &encoderDesc);
if (timestampCount >= 1)
wgpuCommandEncoderWriteTimestamp(encoder, querySet, 0);
if (!minimal)
wgpuCommandEncoderCopyBufferToBuffer(encoder, source, 0, sink, 0, kPayloadBytes);
if (timestampCount >= 2)
wgpuCommandEncoderWriteTimestamp(encoder, querySet, 1);
if (!minimal)
{
wgpuCommandEncoderResolveQuerySet(encoder, querySet, 0, 2, resolved, 0);
wgpuCommandEncoderCopyBufferToBuffer(encoder, resolved, 0, readback, 0, kResultBytes);
}
WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, NULL);
if (!commands)
die("commandEncoderFinish returned NULL");
printf("submitting...\n");
fflush(stdout);
arm_watchdog();
WGPUSubmissionIndex submission = wgpuQueueSubmitForIndex(queue, 1, &commands);
wgpuDevicePoll(device, 1 /* wait */, &submission);
disarm_watchdog();
printf("submission %llu completed\n", (unsigned long long) submission);
if (minimal)
{
/* Nothing was resolved, so there is nothing to read back: reaching
this line at all means the wedge did not happen. */
printf("PASS: submission completed\n");
return 0;
}
int mapped = 0;
WGPUBufferMapCallbackInfo mapCallback;
memset(&mapCallback, 0, sizeof(mapCallback));
mapCallback.mode = WGPUCallbackMode_AllowProcessEvents;
mapCallback.callback = on_map;
mapCallback.userdata1 = &mapped;
wgpuBufferMapAsync(readback, WGPUMapMode_Read, 0, (size_t) kResultBytes, mapCallback);
arm_watchdog();
while (!mapped)
{
wgpuDevicePoll(device, 1 /* wait */, NULL);
wgpuInstanceProcessEvents(instance);
}
disarm_watchdog();
const uint64_t* ticks =
(const uint64_t*) wgpuBufferGetConstMappedRange(readback, 0, (size_t) kResultBytes);
if (!ticks)
die("getConstMappedRange returned NULL");
printf("timestamp[0] = %llu ticks\n", (unsigned long long) ticks[0]);
printf("timestamp[1] = %llu ticks\n", (unsigned long long) ticks[1]);
if (timestampCount == 0)
{
/* No timestamp was ever written, so zeros are expected here; the
point of the control run is that the submission completes. */
printf("PASS: control run completed (no timestamps written)\n");
return 0;
}
int ok = ticks[0] != 0 && ticks[1] != 0 && ticks[1] >= ticks[0];
printf("%s\n", ok ? "PASS: two nonzero, nondecreasing timestamps"
: "FAIL: timestamps are zero or decreasing");
return ok ? 0 : 1;
}
Environment
wgpu-macos-aarch64-releaseprebuilt)Summary
With
timestamp-queryand the nativeTimestampQueryInsideEncodersfeature both advertised by the adapter and both accepted byrequestDevice, a call towgpuCommandEncoderWriteTimestampat command-encoder level (outside any pass) produces a submission that never completes:wgpuDevicePoll(wait = true)on the submission index blocks indefinitely. No validation error and no device-lost callback is reported.Bisected:
writeTimestampand nothing else — no copy, noresolveQuerySet, no readback.writeTimestampcalls completes normally.wgpuQueueSubmit+wgpuDevicePoll(wait, NULL)blocks identically towgpuQueueSubmitForIndex+ poll-on-index, so it is not an artifact of the native submit/poll extras.Expected vs actual
Expected: the submission completes and the queries resolve to nonzero, nondecreasing timestamps.
Actual: the submission never completes (a 10 s watchdog in the reproducer exits the process; without it, the wait blocks forever).
Possibly related
This may share a root cause with gfx-rs/wgpu#9414 (Metal timestamp queries returning zeros on macOS 26, attributed to the macOS 26 counter-sampling changes), but the symptom here is stronger — a hang rather than zeros — so filing separately.
Reproducer
Self-contained C program below; build against a stock wgpu-native install:
main.c
Observed output