minigpu_ffi_completion library

COMPLETION DELIVERY FOR ASYNC GPU WORK.

Every async minigpu entry point starts work on the WebGPU worker thread and signals Dart when it finishes. How that signal travels is the whole subject of this file, because the obvious way is unsound.

Why not a NativeCallable

final nc = NativeCallable<Void Function()>.listener(cb);
try {
  ffi.mgpuSomethingAsync(..., nc.nativeFunction);
  await completer.future;
} finally {
  nc.close();            // ← THE BUG
}

close() DELETES the trampoline, and the C layer cannot be told to forget a pointer it has already been handed — there is no cancel. So the finally is a bet that native will never call again, and the VM's penalty for losing it is not an exception:

runtime_entry.cc: error: Callback invoked after it has been deleted.

That is an unconditional FATAL: the whole process dies, uncatchable, and Flutter reports it as Lost connection to device. Worse, close() is not the only deleter — isolate teardown deletes every callback the isolate owns, so hot restart, a hard kill, and any worker isolate exiting mid-work are all fatal too, and no amount of Dart-side discipline can cover them.

The port

Posting to a Dart port that is closed, or whose isolate is gone, is a defined, silent no-op. It is thread-safe from any thread, with or without an isolate — which is exactly what the WebGPU worker and Dawn's own threads are. So completions travel as a port message, and the failure mode above stops existing rather than being guarded against. The log stream already made this move for the same reason; see minigpu_ffi_log_port.dart.

WIRE FORMAT: one int64 per completion, (token << 1) | ok. Tokens are allocated here, never reused, and dropped from _pending as soon as they resolve — so a late or duplicate completion is a map miss rather than somebody else's future resolving early.

The fallback

A build without the vendored Dart API (the Emscripten/web library) cannot post to a port, so VoidCompletionSlot / IntCompletionSlot remain: pooled NativeCallables that are recycled but never closed, which removes the abort even though it cannot remove the trampoline. They are the fallback, not the default.

Classes

GpuCompletion
Issues async GPU work and awaits its completion.
IntCompletionSlot
A pooled void(int) completion callback for the no-port fallback — the shared-texture blits report success as an int. Same lifetime rules as VoidCompletionSlot.
MgpuElementType
Element type codes for mgpuReadAsyncToPort.
VoidCompletionSlot
A pooled void() completion callback for the no-port fallback.

Functions

mgpuContextInitializeAsyncToPort(Pointer<MGPUContextHandle> handle, int port, int token) → void
mgpuCopyBufferToSharedOutputTextureAsyncToPort(Pointer<MGPUBuffer> buf, Pointer<MGPUSharedOutputTexture> dst, int port, int token) → void
mgpuDispatchAsyncToPort(Pointer<MGPUComputeShader> shader, int groupsX, int groupsY, int groupsZ, int port, int token) → void
mgpuDrainWorkQueue() → void
Blocks until every task already queued on the WebGPU worker thread has run.
mgpuInitializeContextAsyncToPort(int port, int token) → void
mgpuReadAsyncToPort(Pointer<MGPUBuffer> buffer, Pointer<Void> outputData, int elementCount, int elementOffset, int elementType, int port, int token) → void
mgpuVideoTextureBGRAToRGBASharedOutputAsyncToPort(Pointer<MGPUVideoTexture> src, Pointer<MGPUSharedOutputTexture> dst, int port, int token) → void

Typedefs

IntCallbackIssue = void Function(Pointer<NativeFunction<Void Function(Int)>> callback)
…or with a void(int) callback pointer, for work that reports success.
PortIssue = void Function(int port, int token)
How an operation is started when the port path is available.
VoidCallbackIssue = void Function(Pointer<NativeFunction<Void Function()>> callback)
How it is started when it is not: with a void() callback pointer.