Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions thermion_dart/hook/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ outputDirectory : ${outputDirectory.path}
sources = sources.where((p) => !p.contains("linux")).toList();
}

// iOS is Metal-only — exclude Vulkan-utility sources whose symbols
// resolve through `bluevk` (which iOS does not link). Without this
// exclusion, linking fails with "Undefined symbols: bluevk::vk*".
// See native/src/vulkan/{VulkanUtils,BaseVulkanTexture}.cpp.
if (targetOS == OS.iOS) {
sources = sources.where((p) => !p.contains("vulkan")).toList();
}

// Material source paths (used by _processMaterials below)
final materialSources = <String, String>{
'capture_uv': 'native/include/material/capture_uv.c',
Expand All @@ -110,7 +118,11 @@ outputDirectory : ${outputDirectory.path}
'edge_outline': 'native/include/material/edge_outline.c',
'wireframe': 'native/include/material/wireframe.c',
'translation_axis': 'native/include/material/translation_axis.c',
'gizmo': 'native/include/material/gizmo.c',
// Renamed from gizmo.c to avoid a case-insensitive .obj collision
// with scene/Gizmo.cpp on Windows (both produced gizmo.obj, the
// material write-clobbered the class .obj, and the linker reported
// four LNK2019s for thermion::Gizmo::{Gizmo,pick,highlight,unhighlight}).
'gizmo': 'native/include/material/gizmo_material.c',
'bone_overlay': 'native/include/material/bone_overlay.c',
};

Expand Down Expand Up @@ -252,7 +264,11 @@ outputDirectory : ${outputDirectory.path}
"/std:c++20",
if (buildMode == BuildMode.debug) ...["/MDd", "/Zi"],
if (buildMode == BuildMode.release) "/MD",
"/VERBOSE",
// /VERBOSE is a linker option, not a compiler one — cl.exe parses it
// as the deprecated /V<string> and emits warning D9035. If the
// verbose link map is ever needed for diagnostics, pass it after
// native_toolchain_c's own /link separator (see libraryDirectories
// / linkerOptions paths in run_cbuilder.dart).
...defines.keys.map((k) => "/D$k=${defines[k]}").toList(),
]);
}
Expand Down Expand Up @@ -418,10 +434,17 @@ outputDirectory : ${outputDirectory.path}
if (platform == "windows") ...[
...includeDirs.map((d) => "/I${path.join(pkgRootFilePath, d)}"),
"@${srcs.uri.toFilePath(windows: true)}",
// ...sources,
// '/link',
// "/LIBPATH:$libDir",
// '/DLL',
// Library inputs (filament.lib, backend.lib, bluevk.lib, etc.)
// are declared via #pragma comment(lib, ...) directives in
// native/include/ThermionWin32.h, which is transitively included
// by the c_api headers and the Windows vulkan/d3d sources. The
// linker only needs to know WHERE to find those .lib files —
// that is wired via `libraryDirectories: [libDir]` below, which
// native_toolchain_c emits after its own /link separator
// (run_cbuilder.dart). Adding a second /link here puts cl.exe's
// auto-generated /LD and /Fe: AFTER our separator, where LINK
// ignores them as LNK4044 — the resulting binary has no /DLL
// and no entry point, failing with LNK1561.
],
],
libraryDirectories: [libDir],
Expand Down
20 changes: 20 additions & 0 deletions thermion_dart/hook/link.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ void main(List<String> args) async {
var pkgRootFilePath = packageRoot.toFilePath(windows: Platform.isWindows);
final logger = createLogger(pkgRootFilePath, "link.log");

// The CLinker.library(... LinkerOptions.manual(...)) call below
// delegates to native_toolchain_c.runCl on Windows, which builds a
// cl.exe command line from the constructor's `sources` list. Our
// call passes no sources (the build hook already produced
// thermion_dart.dll), so cl.exe is invoked with only flags and exits
// immediately with `cl : Command line error D8003: missing source
// filename`. The link phase is optional here — pass the build
// hook's code assets through unchanged on Windows. Keep the
// CLinker call on platforms where it currently works.
if (input.config.code.targetOS == OS.windows) {
for (final asset in input.assets.code) {
output.assets.code.add(asset);
}
logger.info(
"Link step skipped on Windows; passed through "
"${input.assets.code.length} code asset(s).",
);
return;
}

final clinker = CLinker.library(
name: "thermion_dart",
linkerOptions: LinkerOptions.manual(stripDebug: false));
Expand Down
24 changes: 20 additions & 4 deletions thermion_dart/hook/log.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,25 @@ Logger createLogger(String packageRoot, String logFilename) {

final logger = Logger("")
..level = Level.ALL
..onRecord.listen((record) => logFile.writeAsStringSync(
record.message + "\n",
mode: FileMode.append,
flush: true));
..onRecord.listen((record) {
logFile.writeAsStringSync(
record.message + "\n",
mode: FileMode.append,
flush: true);
// Tee SEVERE records to stderr so subprocess errors (cl.exe,
// clang, ld) actually surface to whoever's watching the
// build. `native_toolchain_c.runProcess` routes captured
// subprocess stderr through `logger.severe`, but on a
// failure it then throws a `ProcessException` whose message
// is just the command + exit code — the real compiler /
// linker output stays in the build.log file inside the pub
// cache where CI never sees it. Mirroring SEVERE to stderr
// makes the actual error visible without affecting
// successful-build noise (compilers don't emit much stderr
// on success).
if (record.level >= Level.SEVERE) {
stderr.writeln(record.message);
}
});
return logger;
}
20 changes: 18 additions & 2 deletions thermion_dart/lib/src/bindings/src/ffi.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,24 @@ Future<void> withVoidCallback(
final completer = Completer();
_requests[requestId] = completer;

_voidCallbackNativeCallable =
NativeCallable<Void Function(Int32)>.listener(_voidCallbackHandler);
// Use the module-scoped `_voidCallbackNativeCallable` initialised at
// file load. Do NOT reassign it per call: every reassignment orphans
// the previous NativeCallable, which native code may still hold a
// pointer to. When Dart GC sweeps the orphan, its trampoline
// metadata is freed; the next invocation from the native side
// calls a removed trampoline, hits the
// `DLRT_GetFfiCallbackMetadata` release-assert, and the VM aborts.
// On Windows multi-viewer (8 viewers × ~60 fps × multiple FFI
// awaits per frame), churn is high enough that GC reliably sweeps
// a stale entry within ~20 seconds and the embedder dies — observed
// as "Not Responding" on the main window. The single global
// listener's handler (`_voidCallbackHandler`) is stateless; it
// dispatches by `requestId` through `_requests`, so one trampoline
// is sufficient and correct.
//
// Upstream introduced the reassignment in 760ae8ed8 as a drive-by
// change inside an unrelated commit ("add makeInt32List method").
// To file upstream once verified.
func.call(requestId, _voidCallbackNativeCallable.nativeFunction.cast());

await completer.future;
Expand Down
70 changes: 64 additions & 6 deletions thermion_dart/native/src/vulkan/windows/WindowsVulkanContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,29 @@ class WindowsVulkanContext::Impl {
// submitInfo.signalSemaphoreCount = 1;
// submitInfo.pSignalSemaphores = &sharedSemaphore;

// Wait for any previous blit to complete and reset fence
result = bluevk::vkWaitForFences(device, 1, &blitFence, VK_TRUE, UINT64_MAX);
// Wait for any previous blit to complete and reset fence.
//
// Bounded to 50 ms. With UINT64_MAX we deadlocked the whole
// app on Windows multi-viewer at sustained 60 fps × N
// viewers: Blit runs on the Flutter UI thread (called from
// ThermionFlutterPlugin::HandleMethodCall for
// markTextureFrameAvailable), the keyed-mutex-style fence
// chain between Vulkan (writer) and D3D11 (Flutter
// compositor reader on its own raster thread) needs the
// UI thread to pump Win32 messages for the compositor to
// advance, and the GPU can't signal this fence until the
// compositor has consumed the previous frame. Three-way
// wait — UI thread → GPU → compositor → UI thread.
//
// On timeout, skip this frame's blit entirely so the UI
// thread returns, the Win32 message pump dispatches,
// Flutter's compositor drains, and the next Blit retries.
// Visual effect is one stale frame on a backed-up GPU;
// far preferable to a hard hang.
result = bluevk::vkWaitForFences(device, 1, &blitFence, VK_TRUE, 50'000'000);
if (result == VK_TIMEOUT) {
return;
}
if (result != VK_SUCCESS) {
std::cerr << "vkWaitForFences failed: " << result << std::endl;
return;
Expand All @@ -428,9 +449,23 @@ class WindowsVulkanContext::Impl {
return;
}

// Wait for blit to complete before returning
result = bluevk::vkWaitForFences(device, 1, &blitFence, VK_TRUE, UINT64_MAX);
if (result != VK_SUCCESS) {
// Wait for blit to complete before returning.
//
// Bounded to 50 ms for the same reason as above. On
// timeout we return without an error: the submit has
// been accepted by the queue, the keyed mutex on the
// shared texture will serialise D3D11's read on its own
// thread, and the *next* call's pre-submit wait above
// will catch up. Returning unblocks the UI thread.
bool postBlitTimedOut = false;
result = bluevk::vkWaitForFences(device, 1, &blitFence, VK_TRUE, 50'000'000);
if (result == VK_TIMEOUT) {
postBlitTimedOut = true;
std::cerr << "vkWaitForFences (post-blit) timed out after 50 ms; "
"GPU still processing. Returning to unblock UI thread; "
"next Blit will sync."
<< std::endl;
} else if (result != VK_SUCCESS) {
std::cerr << "vkWaitForFences (post-blit) failed: " << result << std::endl;
}

Expand All @@ -439,14 +474,37 @@ class WindowsVulkanContext::Impl {
// owns them), so clearing _graveyardRT just frees the wrapper.
// The D3D and interop textures are freed here after the driver
// has retired all references.
if (!_graveyardD3D.empty()) {
//
// SKIP the drain entirely if the post-blit wait just timed
// out — `vkDeviceWaitIdle` has NO timeout parameter in the
// Vulkan API, and if the GPU is busy enough that our
// bounded fence wait timed out, `vkDeviceWaitIdle` will
// block this thread (the Flutter UI thread) forever, which
// is exactly the original hang we're trying to fix.
// Defer the drain to a future Blit call when the GPU
// has caught up. The graveyard accumulates briefly but
// bounded: drain resumes as soon as steady-state catches
// up. Diagnostic stderr around the drain confirms whether
// this skip path is being hit and whether `vkDeviceWaitIdle`
// ever returns when we do invoke it.
if (!_graveyardD3D.empty() && !postBlitTimedOut) {
_graveyardFrames++;
if (_graveyardFrames >= GRAVEYARD_DRAIN_FRAMES) {
std::cerr << "Blit: graveyard drain start (size="
<< _graveyardD3D.size()
<< ", frames=" << _graveyardFrames << ")"
<< std::endl;
bluevk::vkDeviceWaitIdle(device);
std::cerr << "Blit: graveyard drain done (vkDeviceWaitIdle returned)"
<< std::endl;
_graveyardRT.clear();
_graveyardVk.clear();
_graveyardD3D.clear();
}
} else if (!_graveyardD3D.empty() && postBlitTimedOut) {
std::cerr << "Blit: graveyard drain DEFERRED (postBlit timed out, size="
<< _graveyardD3D.size() << ")"
<< std::endl;
}
}

Expand Down
Loading
Loading