fix(renderer): macOS TBDR cross-context sync and OFX instance thread-safety

This commit fixes crashes (SIGSEGV in CImg blur_bilateral) and black-frame
corruption artifacts during playback and scrubbing on macOS Apple Silicon.

Root cause analysis:
1. macOS uses Tile-Based Deferred Rendering (TBDR). glFlush() does not
   guarantee tile memory writeback, causing glReadPixels to read incomplete
   tiles (black/corrupted frames) and cache them to disk.
2. Olive uses multiple shared OpenGL contexts (RenderProcessor contexts vs.
   thread-local PluginRenderer context). glFinish() only waits for the
   current context, not the shared context that produced the texture. CPU
   readback in PluginRenderer could read partially-rendered tiles.
3. OlivePluginInstance and OliveClipInstance are not thread-safe. Concurrent
   RenderProcessors could corrupt internal QMap/images_ and params_ via
   setInputTexture/renderAction races.

Fixes:
- OpenGLRenderer::Flush() on macOS now uses glFinish() unconditionally.
- OpenGLRenderer::DownloadFromTexture() and OpenGLRenderer::Blit() insert
  glFinish() before readback/detach to ensure tile writeback completes.
- PluginRenderer::RenderPlugin() now acquires a per-instance mutex to
  serialize concurrent OFX render calls.
- Before CPU readback in PluginRenderer, flush the renderer that originally
  produced each input texture, ensuring cross-context synchronization.
- RenderProcessor::ProcessVideoFootage() flushes after BlitColorManaged.
- Add black-frame detection in ProcessVideoCacheJob() to auto-purge TBDR-
   corrupted cache files.
- Add diagnostic qDebug() logging in viewer, decoder, renderer, and plugin
  paths to aid future debugging.
This commit is contained in:
2026-05-17 14:54:49 +08:00
parent ff0eee3a88
commit 2a84027ff9
7 changed files with 147 additions and 8 deletions
+24 -7
View File
@@ -205,6 +205,9 @@ void ViewerWidget::TimeChangedEvent(const rational &time)
if (GetConnectedNode() && last_time_ != time) {
if (!IsPlaying()) {
qDebug() << "[VIEWER] TimeChanged seeking to" << time.toDouble()
<< "frame_exists=" << FrameExistsAtTime(time)
<< "might_be_still=" << ViewerMightBeAStill();
UpdateTextureFromNode();
PushScrubbedAudio();
@@ -1445,16 +1448,30 @@ void ViewerWidget::WindowAboutToClose()
void ViewerWidget::RendererGeneratedFrame()
{
RenderTicketWatcher *ticket = static_cast<RenderTicketWatcher *>(sender());
rational t = ticket->property("time").value<rational>();
bool has_result = ticket->HasResult();
qDebug() << "[VIEWER] RendererGeneratedFrame time=" << t.toDouble()
<< "has_result=" << has_result
<< "nonqueue_size=" << nonqueue_watchers_.size();
if (ticket->HasResult()) {
if (nonqueue_watchers_.contains(ticket)) {
while (!nonqueue_watchers_.isEmpty()) {
// Pop frames that are "old"
if (nonqueue_watchers_.takeFirst() == ticket) {
break;
}
if (nonqueue_watchers_.contains(ticket)) {
while (!nonqueue_watchers_.isEmpty()) {
// Pop frames that are "old"
if (nonqueue_watchers_.takeFirst() == ticket) {
break;
}
}
if (ticket->HasResult()) {
QVariant v = ticket->Get();
bool is_tex = v.canConvert<TexturePtr>();
bool is_frame = v.canConvert<FramePtr>();
TexturePtr tex = v.value<TexturePtr>();
qDebug() << "[VIEWER] SetDisplayImage time=" << t.toDouble()
<< "is_texture=" << is_tex
<< "is_frame=" << is_frame
<< "tex_null=" << (tex == nullptr)
<< "tex_dummy=" << (tex ? tex->IsDummy() : true);
SetDisplayImage(ticket->GetTicket());
}
}