Fix image export on macOS (hang / NSView crash) — closes #403 - #411
Fix image export on macOS (hang / NSView crash) — closes #403#411calumk wants to merge 1 commit into
Conversation
Image export was unusable on macOS: `mayo-conv` hung forever and the
desktop app aborted on an NSView assertion. Three distinct defects were
involved.
1. Cocoa_Window cannot be used for offscreen rendering
graphicsCreateVirtualWindow() created a Cocoa_Window, whose
constructor throws Aspect_WindowDefinitionError("Cocoa application
should be instantiated before window") when NSApp is null. That is
always the case in mayo-conv, which builds a QCoreApplication.
Cocoa_Window also allocates a real on-screen NSWindow and retains its
contentView. Export tasks run on a worker thread, and touching NSView
off the main thread is illegal, which produced the assertion reported
in fougue#403:
Assertion failed: (NSViewIsCurrentlyBuildingLayerTreeForDisplay()
!= currentlyBuildingLayerTree), NSView.m:13477
Use Aspect_NeutralWindow on macOS instead, merging with the existing
Android branch. Mayo already relies on Aspect_NeutralWindow elsewhere
(see OcctNeutralWindow in qtopengl_utils.cpp).
2. The exception turned into an infinite hang
TaskManager::Private::execEntity() invoked the job unguarded, so the
throw escaped into the std::async future. That future is stored but
never waited on, so the exception was silently swallowed, signalEnded
never fired, exportTaskCount never reached zero, and
QCoreApplication::exec() blocked forever in poll().
Guard the job call so signalEnded is always emitted, wrap writer
transfer/write in a Standard_Failure-aware helper in io_system.cpp,
and use gsl::finally in cli_export.cpp so the task counter is always
decremented.
3. ToPixMap() failed on a stale GL error
A drawable-less GL context has an incomplete default framebuffer, so
the glDrawBuffer(GL_BACK) call in OpenGl_Window::init() raises
GL_INVALID_FRAMEBUFFER_OPERATION and never clears it.
OpenGl_Texture::Init() later reads that stale error while building the
dump FBO and wrongly reports:
2D texture 128x128 IF: GL_SRGB8_ALPHA8 PF: GL_RGBA
DT: GL_UNSIGNED_BYTE can not be created with error
GL_INVALID_FRAMEBUFFER_OPERATION
This is a false negative: an isolated probe confirmed the texture and
FBO are created correctly (FBO status GL_FRAMEBUFFER_COMPLETE) and
read back the expected pixels.
Add graphicsPrepareVirtualWindowRendering(), which makes the context
current and clears pending errors. It must be called immediately
before ToPixMap(); doing it only at view creation is too early
because Redraw() re-raises the error.
Also report real errors from ImageWriter::writeFile() instead of
returning false silently.
Tested on macOS 26.5 arm64, Qt 6.10.2, OpenCascade 7.9.3, for both
mayo-conv and the desktop app. Export verified for STEP, IGES, BREP,
OBJ, STL, PLY and glTF inputs, at sizes up to 3840x2160, including
concurrent exports and invalid-input error paths. Test suite: 225
passed, 0 failed. Not tested on Linux or Windows.
Written by Claude Opus 5 operating/steered as instructed on behalf of
@calumk.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
| } | ||
| catch (const Standard_Failure& err) { | ||
| return fnError( | ||
| fmt::format("Exception '{}': {}", err.DynamicType()->Name(), err.GetMessageString()) |
There was a problem hiding this comment.
Compilation fails with OpenCascade 8.0.0 (latest version) because of API change
Please do :
fmt::format("Exception '{}': {}", TKernelUtils::errorTypeName(err), TKernelUtils::errorMessage(err))
|
|
||
| const bool okSave = pixmap->Save(filepathTo<TCollection_AsciiString>(filepath)); | ||
| if (!okSave) { | ||
| this->messenger()->emitError(fmt::format( |
There was a problem hiding this comment.
This message doesn't need the surrounding fmt::format()
|
Is it possible to remove "Claude" as co-author ? I dislike the idea an AI to be member of contributors in Mayo repo |
|
@calumk |
Of course! Feel free to close, pull from this, or ignore it completely etc - As i said most of this was not really my ground work, it was just me digging through and itterating / testing with to find the big and some route causes - But it did at least get a functioning system working I dont need to be credited in any way, the only goal for me is that mayo becomes a little more stable on mac :) |
Thanks, then I'm going to have a deeper look of your 2 PRs |
|
Can you please test my branch that incorporates changes of your PR ?
|
Fixes #403.
Summary
Image export was unusable on macOS.
mayo-convhung forever, and the desktop app aborted with theNSView.m:13477assertion @HuguesDelorme reproduced on Monterey. There turned out to be three independent defects stacked on top of each other — fixing any one alone still leaves export broken.1.
Cocoa_Windowcannot be used for offscreen renderinggraphicsCreateVirtualWindow()created aCocoa_Window. Its constructor throws whenNSAppis null:NSAppis always null inmayo-conv, which builds aQCoreApplication. Caught with an lldb throw breakpoint:Cocoa_Windowalso allocates a real on-screenNSWindowand retains itscontentView. Export runs on a worker thread, and touchingNSViewoff the main thread is illegal — this is the source of the assertion crash in the desktop app, which is why the GUI failed differently from the CLI.Fix: use
Aspect_NeutralWindowon macOS, merging with the existing Android branch. Mayo already relies onAspect_NeutralWindowelsewhere (OcctNeutralWindowinqtopengl_utils.cpp).2. The exception became an infinite hang
This explains the "hangs, file is not generated" symptom rather than an error message.
TaskManager::Private::execEntity()called the job unguarded. The throw escaped into thestd::asyncfuture — which is stored but never waited on, so the exception was silently swallowed. ConsequentlysignalEndednever fired →exportTaskCountnever reached zero →qtApp->exit()was never called →QCoreApplication::exec()blocked forever inpoll().Fix, defence in depth:
task_manager.cpp— guard the job call sosignalEndedis always emitted andisFinishedalways set.io_system.cpp— wrap writertransfer/writeFilein a helper that catchesStandard_Failure(reportingDynamicType()->Name()+GetMessageString()),std::exceptionand....cli_export.cpp—gsl::finallyso the task counter is always decremented.Any OCCT throw in an export path now surfaces as a proper error message instead of a hang.
3.
ToPixMap()failed on a stale GL errorOnly visible after #1 was fixed:
A drawable-less GL context has an incomplete default framebuffer, so OCCT's own
glDrawBuffer(GL_BACK)inOpenGl_Window::init()raisesGL_INVALID_FRAMEBUFFER_OPERATIONand never clears it.OpenGl_Texture::Init()then reads that stale error while building the dump FBO and wrongly concludes texture creation failed.Confirmed a false negative with a standalone probe: a view-less
NSOpenGLContext(GL_VERSION4.1 Metal - 90.5) created the same texture with error0x0, gotFBO status = GL_FRAMEBUFFER_COMPLETE, and read back the expected pixels. A/B test in-tree: without the error resetToPixMapreturns 0; with it, 1.Fix: add
graphicsPrepareVirtualWindowRendering(), which makes the context current and callsResetErrors(). It must be called immediately beforeToPixMap()— doing it only at view creation is too early, becauseRedraw()re-raises the error.Also:
ImageWriter::writeFile()now emits real errors instead ofreturn falsesilently.Testing
macOS 26.5 arm64 (M-series), Qt 6.10.2, OpenCascade 7.9.3, AppleClang 21.
mayo-convand the desktop app both export successfully; no hang, no assertion crash--exporttargets in one invocationCaveats
Aspect_NeutralWindowchange is inside#elif defined(MAYO_OS_MAC) || defined(MAYO_OS_ANDROID)so other platforms are untouched, but thetask_manager.cppandio_system.cppchanges are cross-platform and deserve a look — they are defensive only and change no success-path behaviour.Image_AlienPixMap::Save()writes Netpbm/PPM data whatever the extension (filereportsNetpbm image data, headerP6). That is an OCCT packaging matter, not a Mayo bug, but it does mean.pngoutput is not really PNG on a stock Homebrew install.Related
A fourth, unrelated defect surfaced once export worked: BRep inputs render as wireframe because
mayo-convnever meshes them for image export. That is deliberately not in this PR — filed as #412 with its own fix in #413.#413 also touches
src/cli/cli_export.cpp, so whichever of the two merges second may need a trivial rebase. The hunks are adjacent but distinct: this PR adds agsl::finallyinexportDocument(), #413 changes the meshing predicate inimportInDocument().I acknowledge
CLA.mdapplies to this contribution.