• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 30161608849

25 Jul 2026 02:25PM UTC coverage: 15.303% (-0.008%) from 15.311%
30161608849

Pull #2148

github

web-flow
Merge d2b82a62f into 13afd939a
Pull Request #2148: wasm: stream large media from Blob URLs instead of copying into RAM (Tier B)

4 of 179 new or added lines in 10 files covered. (2.23%)

7 existing lines in 5 files now uncovered.

30396 of 198627 relevant lines covered (15.3%)

983.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/plugins/score-plugin-gfx/Gfx/Graph/DirectVideoNodeRenderer.cpp
1
#include <Gfx/Graph/DirectVideoNodeRenderer.hpp>
2
#include <Gfx/Graph/HWAccelSetup.hpp>
3
#include <Gfx/Graph/VulkanVideoDevice.hpp>
4
#include <Gfx/Settings/Model.hpp>
5

6
#include <score/application/ApplicationContext.hpp>
7
#include <Gfx/Graph/decoders/GPUVideoDecoder.hpp>
8
#include <Gfx/Graph/decoders/GPUVideoDecoderFactory.hpp>
9
#include <Gfx/Graph/decoders/HWTransfer.hpp>
10
#if defined(__linux__)
11
#include <Gfx/Graph/decoders/HWVAAPI.hpp>
12
#endif
13
#include <Gfx/Graph/decoders/HWCUDA.hpp>
14
#include <Gfx/Graph/decoders/HWD3D11.hpp>
15
#include <Gfx/Graph/decoders/HWD3D12.hpp>
16
#include <Gfx/Graph/decoders/HWVideoToolbox.hpp>
17
#include <Gfx/Graph/decoders/HWVulkanShared.hpp>
18

19
#include <Video/GpuFormats.hpp>
20

21
#include <score/tools/Debug.hpp>
22

23
#include <ossia/detail/flicks.hpp>
24
#include <ossia/detail/libav.hpp>
25

26
extern "C" {
27
#include <libavcodec/avcodec.h>
28
#include <libavformat/avformat.h>
29
#include <libavutil/pixdesc.h>
30
#if __has_include(<libavutil/hwcontext.h>)
31
#include <libavutil/hwcontext.h>
32
#endif
33
#if QT_HAS_VULKAN && __has_include(<libavutil/hwcontext_vulkan.h>) && (LIBAVUTIL_VERSION_INT > AV_VERSION_INT(60, 0, 0))
34
#include <libavutil/hwcontext_vulkan.h>
35
#define SCORE_HAS_VULKAN_HWCONTEXT 1
36
#endif
37
#if defined(_WIN32) && __has_include(<libavutil/hwcontext_d3d11va.h>)
38
#include <libavutil/hwcontext_d3d11va.h>
39
#define SCORE_HAS_D3D11_HWCONTEXT 1
40
#endif
41
}
42

43
#if defined(__linux__)
44
#include <dlfcn.h>
45
#endif
46
#if defined(_WIN32) && defined(SCORE_HAS_D3D11_HWCONTEXT)
47
#include <QtGui/private/qrhid3d11_p.h>
48
#endif
49

50
namespace score::gfx
51
{
52

53
DirectVideoNodeRenderer::DirectVideoNodeRenderer(
×
54
    const VideoNodeBase& node, const Video::VideoMetadata& metadata) noexcept
55
    : NodeRenderer{node}
×
56
    , m_filePath{metadata.filePath}
×
57
    , m_fps{metadata.fps}
×
58
    , m_flicks_per_dts{metadata.flicks_per_dts}
×
59
    , m_dts_per_flicks{metadata.dts_per_flicks}
×
60
    , m_frameFormat{metadata}
×
61
{
×
62
  m_frameFormat.output_format = node.m_outputFormat;
×
63
  m_frameFormat.tonemap = node.m_tonemap;
×
64
  m_currentScaleMode = node.m_scaleMode;
×
65
}
66

67
DirectVideoNodeRenderer::~DirectVideoNodeRenderer()
×
68
{
×
69
  closeFile();
×
70
}
×
71

72
TextureRenderTarget DirectVideoNodeRenderer::renderTargetForInput(const Port& input)
×
73
{
74
  return {};
×
75
}
76

77
// ============================================================
78
//  Hardware acceleration selection
79
// ============================================================
80

81
#if LIBAVUTIL_VERSION_MAJOR >= 57
82

83
// get_format callback for AVCodecContext.
84
// When HW decoding is active, ffmpeg calls this to negotiate the output pixel format.
85
// We select the HW format that matches our desired acceleration.
86
// Static member function — compatible with C function pointer assignment in practice.
87
enum AVPixelFormat DirectVideoNodeRenderer::negotiateHWFormat(
×
88
    AVCodecContext* ctx, const enum AVPixelFormat* pix_fmts)
89
{
90
  auto* self = static_cast<DirectVideoNodeRenderer*>(ctx->opaque);
×
91
  if(!self)
×
92
    return ctx->pix_fmt;
×
93

94
  for(const auto* p = pix_fmts; *p != AV_PIX_FMT_NONE; ++p)
×
95
  {
96
    if(*p == self->m_hwPixelFormat)
×
97
      return *p;
×
98
  }
×
99

100
  // HW format not offered — fall back to default
101
  qDebug() << "DirectVideoNodeRenderer: HW format not offered by decoder, falling back";
×
102
  return ctx->pix_fmt;
×
103
}
×
104

105
AVPixelFormat DirectVideoNodeRenderer::selectHardwareAcceleration(
×
106
    score::gfx::GraphicsApi api, int codec_id) const
107
{
108
  return score::gfx::selectHardwareAcceleration(
×
109
      api, static_cast<AVCodecID>(codec_id), m_rhi);
×
110
}
111

112
bool DirectVideoNodeRenderer::setupHardwareDecoder(
×
113
    const void* detected_codec_ptr, AVPixelFormat hwPixFmt)
114
{
115
  const auto* detected_codec = static_cast<const AVCodec*>(detected_codec_ptr);
×
116
  auto hwInfo = Video::ffmpegHardwareDecodingFormats(hwPixFmt);
×
117
  if(hwInfo.device == AV_HWDEVICE_TYPE_NONE)
×
118
    return false;
×
119

120
  // Find the appropriate hardware codec
121
  auto mapped = score::gfx::hwCodecName(detected_codec->name, hwInfo.device);
×
122
  if(mapped.empty())
×
123
    return false;
×
124

125
  const AVCodec* hw_codec = nullptr;
×
126
  if(mapped == detected_codec->name)
×
127
  {
128
    // VideoToolbox, DXVA2, D3D11VA: use generic codec with hw_device_ctx
129
    hw_codec = detected_codec;
×
130
  }
×
131
  else
132
  {
133
    hw_codec = avcodec_find_decoder_by_name(mapped.c_str());
×
134
    if(!hw_codec)
×
135
      return false;
×
136
  }
137

138
  // DRM_PRIME (v4l2m2m) doesn't use a hw device context
139
  if(hwPixFmt == AV_PIX_FMT_DRM_PRIME)
×
140
  {
141
    // Re-create codec context with the hw codec
142
    avcodec_free_context(&m_codecContext);
×
143
    m_codecContext = avcodec_alloc_context3(hw_codec);
×
144
    avcodec_parameters_to_context(m_codecContext, m_avstream->codecpar);
×
145
    m_codecContext->pkt_timebase = m_avstream->time_base;
×
146
    m_codecContext->thread_count = 1;
×
147

148
    int err = avcodec_open2(m_codecContext, hw_codec, nullptr);
×
149
    if(err < 0)
×
150
    {
151
      qDebug() << "DirectVideoNodeRenderer: v4l2m2m open failed:" << err;
×
152
      avcodec_free_context(&m_codecContext);
×
153
      return false;
×
154
    }
155

156
    m_hwPixelFormat = hwPixFmt;
×
157
    m_codec = hw_codec;
×
158
    return true;
×
159
  }
160

161
  // Ensure CUDA driver is initialized before FFmpeg tries to use it.
162
  // FFmpeg's hwcontext_cuda calls cuInit() internally, but in some setups
163
  // it needs to be called earlier in the process lifetime.
164
  if(hwInfo.device == AV_HWDEVICE_TYPE_CUDA)
×
165
  {
166
    static const bool cuInitOk = []() {
×
167
#if defined(__linux__)
168
      void* lib = dlopen("libcuda.so.1", RTLD_NOW);
×
169
      if(!lib)
×
170
        return false;
×
171
      using FN_cuInit = int (*)(unsigned int);
172
      auto fn = (FN_cuInit)dlsym(lib, "cuInit");
×
173
      if(!fn)
×
174
      {
175
        qDebug("no cuInit!");
×
176
        return false;
×
177
      }
178
      auto res = fn(0);
×
179
      qDebug() << res;
×
180

181
      return fn && fn(0) == 0;
×
182
      return fn && fn(0) == 0;
183
      // Intentionally keep library loaded
184
#elif defined(_WIN32)
185
      HMODULE lib = LoadLibraryA("nvcuda.dll");
186
      if(!lib)
187
        return false;
188
      using FN_cuInit = int(__stdcall*)(unsigned int);
189
      auto fn = (FN_cuInit)GetProcAddress(lib, "cuInit");
190
      if(!fn)
191
        qDebug("no cuInit!");
192
      auto res = fn(0);
193
      qDebug() << res;
194

195
      return fn && fn(0) == 0;
196
#else
197
      return false;
198
#endif
199
    }();
×
200
    if(!cuInitOk)
×
201
    {
202
      qDebug() << "DirectVideoNodeRenderer: cuInit(0) failed";
×
203
      return false;
×
204
    }
205
    else
206
    {
207
      qDebug("CUINIT OK");
×
208
    }
209
  }
×
210

211
  // Create hardware device context
212
  AVBufferRef* hw_device_ctx = nullptr;
×
213

214
  // For Vulkan: try to create a shared context using QRhi's device.
215
  // This enables zero-copy AVVkFrame wrapping (no DMA-BUF export needed).
216
#if QT_HAS_VULKAN && defined(SCORE_HAS_VULKAN_HWCONTEXT) \
217
    && QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
218
  if(hwInfo.device == AV_HWDEVICE_TYPE_VULKAN
219
     && m_rhi && m_rhi->backend() == QRhi::Vulkan)
220
  {
221
    auto* nh
222
        = static_cast<const QRhiVulkanNativeHandles*>(m_rhi->nativeHandles());
223
    if(nh && nh->dev && nh->physDev && nh->inst)
224
    {
225
      hw_device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VULKAN);
226
      if(hw_device_ctx)
227
      {
228
        auto* devCtx = reinterpret_cast<AVHWDeviceContext*>(hw_device_ctx->data);
229
        auto* vkCtx
230
            = static_cast<AVVulkanDeviceContext*>(devCtx->hwctx);
231

232
        // Fill device handles from QRhi
233
        vkCtx->inst = nh->inst->vkInstance();
234
        vkCtx->phys_dev = nh->physDev;
235
        vkCtx->act_dev = nh->dev;
236

237
        // Use the system Vulkan loader's vkGetInstanceProcAddr.
238
        // Qt's getInstanceProcAddr dispatch doesn't include all extension
239
        // functions (push_descriptor, drm_format_modifier, etc.).
240
        // The system loader resolves them correctly for any VkDevice.
241
#if defined(__linux__)
242
        {
243
          static void* libvk = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_NOLOAD);
244
          if(!libvk)
245
            libvk = dlopen("libvulkan.so.1", RTLD_NOW);
246
          if(libvk)
247
            vkCtx->get_proc_addr = reinterpret_cast<PFN_vkGetInstanceProcAddr>(
248
                dlsym(libvk, "vkGetInstanceProcAddr"));
249
        }
250
#elif defined(_WIN32)
251
        {
252
          static HMODULE libvk = GetModuleHandleA("vulkan-1.dll");
253
          if(!libvk)
254
            libvk = LoadLibraryA("vulkan-1.dll");
255
          if(libvk)
256
            vkCtx->get_proc_addr = reinterpret_cast<PFN_vkGetInstanceProcAddr>(
257
                GetProcAddress(libvk, "vkGetInstanceProcAddr"));
258
        }
259
#endif
260
        if(!vkCtx->get_proc_addr)
261
        {
262
          qDebug() << "DirectVideoNodeRenderer: could not load vkGetInstanceProcAddr";
263
          av_buffer_unref(&hw_device_ctx);
264
          hw_device_ctx = nullptr;
265
        }
266

267
        if(hw_device_ctx)
268
        {
269
        // Report only the extensions that were actually enabled on
270
        // the shared device (the curated list from createSharedVulkanDevice).
271
        // Reporting unavailable extensions causes FFmpeg to resolve null
272
        // function pointers → crash.
273
        auto* funcs = nh->inst->functions();
274
        uint32_t availExtCount = 0;
275
        funcs->vkEnumerateDeviceExtensionProperties(
276
            nh->physDev, nullptr, &availExtCount, nullptr);
277
        std::vector<VkExtensionProperties> availExts(availExtCount);
278
        funcs->vkEnumerateDeviceExtensionProperties(
279
            nh->physDev, nullptr, &availExtCount, availExts.data());
280

281
        auto devHasExt = [&](const char* name) {
282
          for(auto& e : availExts)
283
            if(std::strcmp(e.extensionName, name) == 0)
284
              return true;
285
          return false;
286
        };
287

288
        // Same curated list as createSharedVulkanDevice, but exclude
289
        // DMA-BUF extensions — the shared device doesn't need them and
290
        // FFmpeg will try (and fail) DMA-BUF exports if it thinks they're enabled.
291
        // Store pointers to string literals directly — no std::string copy
292
        // needed (sharedVulkanDeviceExtensions returns compile-time constants).
293
        // Using std::vector<std::string> + c_str() would cause dangling
294
        // pointers when the vector reallocates.
295
        m_vkEnabledExtensions.clear();
296
        for(auto* ext : score::gfx::sharedVulkanDeviceExtensions())
297
        {
298
          if(devHasExt(ext))
299
            m_vkEnabledExtensions.push_back(ext);
300
        }
301
        vkCtx->enabled_dev_extensions = m_vkEnabledExtensions.data();
302
        vkCtx->nb_enabled_dev_extensions
303
            = static_cast<int>(m_vkEnabledExtensions.size());
304

305
        // Query queue families for FFmpeg
306
        uint32_t qfCount = 0;
307
        funcs->vkGetPhysicalDeviceQueueFamilyProperties(
308
            nh->physDev, &qfCount, nullptr);
309
        auto qfProps = std::make_unique<VkQueueFamilyProperties[]>(qfCount);
310
        funcs->vkGetPhysicalDeviceQueueFamilyProperties(
311
            nh->physDev, &qfCount, qfProps.get());
312

313
        int nb_qf = 0;
314
        for(uint32_t i = 0; i < qfCount && nb_qf < 64; i++)
315
        {
316
          vkCtx->qf[nb_qf].idx = static_cast<int>(i);
317
          vkCtx->qf[nb_qf].num = 1;
318
          vkCtx->qf[nb_qf].flags
319
              = static_cast<VkQueueFlagBits>(qfProps[i].queueFlags);
320
          nb_qf++;
321
        }
322
        vkCtx->nb_qf = nb_qf;
323

324
        int ret = av_hwdevice_ctx_init(hw_device_ctx);
325

326
        // DO NOT clear enabled_dev_extensions — FFmpeg's vulkan_decode.c
327
        // reads them directly (via ff_vk_extensions_to_mask) during the
328
        // first frame decode, not during av_hwdevice_ctx_init.
329
        // The pointers are string literals, valid for program lifetime.
330

331
        if(ret < 0)
332
        {
333
          qDebug() << "DirectVideoNodeRenderer: shared Vulkan context init failed:"
334
                   << ret << "- falling back to separate device";
335
          av_buffer_unref(&hw_device_ctx);
336
          hw_device_ctx = nullptr;
337
        }
338
        else
339
        {
340
          qDebug() << "DirectVideoNodeRenderer: using shared Vulkan device for HW decode";
341
          m_sharedVulkanDevice = true;
342
        }
343
        } // if(hw_device_ctx) after get_proc_addr check
344
      }
345
    }
346
  }
347
#endif
348

349
  // For D3D11VA: create a shared context using QRhi's device.
350
  // Without this, FFmpeg creates its own D3D11 device and
351
  // CopySubresourceRegion silently fails across devices (green screen).
352
#if defined(_WIN32) && defined(SCORE_HAS_D3D11_HWCONTEXT)
353
  if(!hw_device_ctx && hwInfo.device == AV_HWDEVICE_TYPE_D3D11VA && m_rhi
354
     && m_rhi->backend() == QRhi::D3D11)
355
  {
356
    auto* nh
357
        = static_cast<const QRhiD3D11NativeHandles*>(m_rhi->nativeHandles());
358
    if(nh && nh->dev)
359
    {
360
      qDebug() << "DirectVideoNodeRenderer: creating shared D3D11 context, QRhi device:" << nh->dev;
361
      hw_device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_D3D11VA);
362
      if(hw_device_ctx)
363
      {
364
        auto* devCtx = reinterpret_cast<AVHWDeviceContext*>(hw_device_ctx->data);
365
        auto* d3d11Ctx
366
            = static_cast<AVD3D11VADeviceContext*>(devCtx->hwctx);
367

368
        d3d11Ctx->device = static_cast<ID3D11Device*>(nh->dev);
369
        d3d11Ctx->device->AddRef();
370

371
        int ret = av_hwdevice_ctx_init(hw_device_ctx);
372
        if(ret < 0)
373
        {
374
          qDebug() << "DirectVideoNodeRenderer: shared D3D11 context init failed:"
375
                   << ret << "- falling back to separate device";
376
          av_buffer_unref(&hw_device_ctx);
377
          hw_device_ctx = nullptr;
378
        }
379
        else
380
        {
381
          // Verify FFmpeg is using the same device
382
          auto* finalCtx = reinterpret_cast<AVHWDeviceContext*>(hw_device_ctx->data);
383
          auto* finalD3D11 = static_cast<AVD3D11VADeviceContext*>(finalCtx->hwctx);
384
          qDebug() << "DirectVideoNodeRenderer: shared D3D11 context OK, FFmpeg device:"
385
                   << finalD3D11->device << "QRhi device:" << nh->dev
386
                   << "same:" << (finalD3D11->device == static_cast<ID3D11Device*>(nh->dev));
387
        }
388
      }
389
    }
390
    else
391
    {
392
      qDebug() << "DirectVideoNodeRenderer: D3D11VA requested but QRhi has no D3D11 native handles";
393
    }
394
  }
395
  else if(!hw_device_ctx && hwInfo.device == AV_HWDEVICE_TYPE_D3D11VA)
396
  {
397
    qDebug() << "DirectVideoNodeRenderer: D3D11VA but no QRhi or wrong backend"
398
             << "m_rhi:" << m_rhi
399
             << "backend:" << (m_rhi ? m_rhi->backend() : -1);
400
  }
401
#endif
402

403
  // Fallback: let FFmpeg create its own device
404
  if(!hw_device_ctx)
×
405
  {
406
    int ret = av_hwdevice_ctx_create(
×
407
        &hw_device_ctx, hwInfo.device, nullptr, nullptr, 0);
×
408
    if(ret != 0)
×
409
    {
410
      qDebug() << "DirectVideoNodeRenderer: av_hwdevice_ctx_create failed:" << ret;
×
411
      return false;
×
412
    }
413
  }
×
414

415
  // Re-create codec context with HW support
416
  avcodec_free_context(&m_codecContext);
×
417

418
  // For CUDA/VDPAU: use the hw-specific codec (e.g., h264_cuvid)
419
  // For DXVA2/D3D11/VideoToolbox: use the generic codec with hw_device_ctx
420
  const AVCodec* codec_to_open = nullptr;
×
421
  if(hwInfo.device == AV_HWDEVICE_TYPE_CUDA
×
422
     || hwInfo.device == AV_HWDEVICE_TYPE_VDPAU
×
423
     || hwInfo.device == AV_HWDEVICE_TYPE_QSV)
×
424
  {
425
    codec_to_open = hw_codec;
×
426
  }
×
427
  else
428
  {
429
    codec_to_open = detected_codec;
×
430
  }
431

432
  m_codecContext = avcodec_alloc_context3(codec_to_open);
×
433
  avcodec_parameters_to_context(m_codecContext, m_avstream->codecpar);
×
434
  m_codecContext->pkt_timebase = m_avstream->time_base;
×
435
  m_codecContext->thread_count = 1;
×
436
  m_codecContext->hw_device_ctx = av_buffer_ref(hw_device_ctx);
×
437
  m_codecContext->opaque = this;
×
438
  m_codecContext->get_format = &DirectVideoNodeRenderer::negotiateHWFormat;
×
439

440
  m_hwPixelFormat = hwPixFmt;
×
441

442
  int err = avcodec_open2(m_codecContext, codec_to_open, nullptr);
×
443
  if(err < 0)
×
444
  {
445
    qDebug() << "DirectVideoNodeRenderer: HW codec open failed:" << err
×
446
             << "for" << codec_to_open->name;
×
447
    avcodec_free_context(&m_codecContext);
×
448
    av_buffer_unref(&hw_device_ctx);
×
449
    m_hwPixelFormat = AV_PIX_FMT_NONE;
×
450
    return false;
×
451
  }
452

453
  m_hwDeviceCtx = hw_device_ctx;
×
454
  m_codec = codec_to_open;
×
455
  return true;
×
456
}
×
457

458
#endif // LIBAVUTIL_VERSION_MAJOR >= 57
459

460
// Transfer a hardware-decoded frame to a software frame.
461
// Used when zero-copy is not available for the current RHI backend.
462
void DirectVideoNodeRenderer::transferHWFrame()
×
463
{
464
#if LIBAVUTIL_VERSION_MAJOR >= 57
465
  if(!m_decodedFrame)
×
466
    return;
×
467

468
  if(!Video::formatIsHardwareDecoded(static_cast<AVPixelFormat>(m_decodedFrame->format)))
×
469
    return;
×
470

471
  if(!m_swTransferFrame)
×
472
    m_swTransferFrame = av_frame_alloc();
×
473

474
  av_frame_unref(m_swTransferFrame);
×
475
  m_swTransferFrame->format = AV_PIX_FMT_NONE;
×
476

477
  int ret = av_hwframe_transfer_data(m_swTransferFrame, m_decodedFrame, 0);
×
478
  if(ret < 0)
×
479
  {
480
    qDebug() << "DirectVideoNodeRenderer: av_hwframe_transfer_data failed:" << ret;
×
481
    return;
×
482
  }
483

484
  m_swTransferFrame->pts = m_decodedFrame->pts;
×
485
  m_swTransferFrame->pkt_dts = m_decodedFrame->pkt_dts;
×
486

487
  // Swap: m_decodedFrame now holds the software frame
488
  av_frame_unref(m_decodedFrame);
×
489
  av_frame_move_ref(m_decodedFrame, m_swTransferFrame);
×
490
#endif
491
}
×
492

493
// ============================================================
494
//  File open / close
495
// ============================================================
496

497
bool DirectVideoNodeRenderer::openFile(score::gfx::GraphicsApi api, QRhi* rhi)
×
498
{
499
  m_rhiApi = api;
×
500
  m_rhi = rhi;
×
501

502
  if(m_filePath.empty())
×
503
    return false;
×
504

NEW
505
  if(auto hook = ossia::libav_custom_open())
×
NEW
506
    m_formatContext = hook(m_filePath.c_str(), m_ioOwner, m_ioFree);
×
507

NEW
508
  if(!m_formatContext
×
NEW
509
     && avformat_open_input(&m_formatContext, m_filePath.c_str(), nullptr, nullptr) != 0)
×
UNCOV
510
    return false;
×
511

512
  if(avformat_find_stream_info(m_formatContext, nullptr) < 0)
×
513
  {
514
    closeFile();
×
515
    return false;
×
516
  }
517

518
  // Find video stream
519
  int stream = -1;
×
520
  for(unsigned int i = 0; i < m_formatContext->nb_streams; i++)
×
521
  {
522
    if(m_formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
×
523
    {
524
      if(stream == -1)
×
525
      {
526
        stream = i;
×
527
        continue;
×
528
      }
529
    }
×
530
    m_formatContext->streams[i]->discard = AVDISCARD_ALL;
×
531
  }
×
532

533
  if(stream == -1)
×
534
  {
535
    closeFile();
×
536
    return false;
×
537
  }
538

539
  m_avstream = m_formatContext->streams[stream];
×
540
  auto codecPar = m_avstream->codecpar;
×
541

542
  // HAP: no codec needed, raw packet data goes directly to GPU
543
  if(codecPar->codec_id == AV_CODEC_ID_HAP)
×
544
  {
545
    m_useAVCodec = false;
×
546
    memcpy(&m_frameFormat.pixel_format, &codecPar->codec_tag, 4);
×
547
    m_frameFormat.width = codecPar->width;
×
548
    m_frameFormat.height = codecPar->height;
×
549
    return true;
×
550
  }
551

552
  // DXV: peek first packet to determine sub-format, then GPU-direct for DXT1/DXT5
553
  if(codecPar->codec_id == AV_CODEC_ID_DXV)
×
554
  {
555
    auto packet = av_packet_alloc();
×
556
    bool dxv_ok = false;
×
557
    if(av_read_frame(m_formatContext, packet) >= 0 && packet->size >= 4)
×
558
    {
559
      uint32_t tag = packet->data[0] | (packet->data[1] << 8)
×
560
                     | (packet->data[2] << 16)
×
561
                     | ((uint32_t)packet->data[3] << 24);
×
562
      switch(tag)
×
563
      {
564
        case 0x44585431: // MKBETAG('D','X','T','1')
565
          memcpy(&m_frameFormat.pixel_format, "Dxv1", 4);
×
566
          dxv_ok = true;
×
567
          break;
×
568
        case 0x44585435: // MKBETAG('D','X','T','5')
569
          memcpy(&m_frameFormat.pixel_format, "Dxv5", 4);
×
570
          dxv_ok = true;
×
571
          break;
×
572
        case 0x59434736: // MKBETAG('Y','C','G','6')
573
          memcpy(&m_frameFormat.pixel_format, "DxvY", 4);
×
574
          dxv_ok = true;
×
575
          break;
×
576
        case 0x59473130: // MKBETAG('Y','G','1','0')
577
          memcpy(&m_frameFormat.pixel_format, "DxvA", 4);
×
578
          dxv_ok = true;
×
579
          break;
×
580
        default: {
581
          // Old format: check type flags in high byte
582
          uint8_t old_type = tag >> 24;
×
583
          if(old_type & 0x40)
×
584
          {
585
            memcpy(&m_frameFormat.pixel_format, "Dxv5", 4);
×
586
            dxv_ok = true;
×
587
          }
×
588
          else if(old_type & 0x20)
×
589
          {
590
            memcpy(&m_frameFormat.pixel_format, "Dxv1", 4);
×
591
            dxv_ok = true;
×
592
          }
×
593
          break;
×
594
        }
595
      }
596
      av_packet_unref(packet);
×
597
    }
×
598
    av_packet_free(&packet);
×
599
    // Seek back to beginning
600
    av_seek_frame(m_formatContext, m_avstream->index, 0, AVSEEK_FLAG_BACKWARD);
×
601

602
    if(dxv_ok)
×
603
    {
604
      m_useAVCodec = false;
×
605
      m_frameFormat.width = codecPar->width;
×
606
      m_frameFormat.height = codecPar->height;
×
607
      return true;
×
608
    }
609
    // else: YCG6/YG10 or unknown — fall through to avcodec
610
  }
×
611

612
  auto codec = avcodec_find_decoder(codecPar->codec_id);
×
613
  if(!codec)
×
614
  {
615
    closeFile();
×
616
    return false;
×
617
  }
618
  m_codec = codec;
×
619

620
  m_codecContext = avcodec_alloc_context3(codec);
×
621
  avcodec_parameters_to_context(m_codecContext, codecPar);
×
622
  m_codecContext->pkt_timebase = m_avstream->time_base;
×
623
  m_codecContext->thread_count = 1;
×
624

625
  // Try hardware-accelerated decoding
626
  bool hw_ok = false;
×
627
#if LIBAVUTIL_VERSION_MAJOR >= 57
628
  {
629
    static constexpr const char* apiNames[]
630
        = {"Null", "OpenGL", "Vulkan", "D3D11", "Metal", "D3D12"};
631
    const char* apiName = (api >= 0 && api <= 5) ? apiNames[api] : "Unknown";
×
632
    uint32_t vendorId = rhi ? rhi->driverInfo().vendorId : 0;
×
633

634
    // Read the user's HW decode setting
635
    static const Gfx::Settings::HardwareVideoDecoder decoders;
×
636
    auto& set = score::AppContext().settings<Gfx::Settings::Model>();
×
637
    const auto hwSetting = set.getHardwareDecode();
×
638

639
    if(hwSetting.isEmpty() || hwSetting == decoders.None)
×
640
    {
641
      // Explicitly disabled
642
      // qDebug() << "DirectVideoNodeRenderer: RHI backend:" << apiName
643
      //          << "HW decode: disabled by user";
644
    }
×
645
    else if(hwSetting == decoders.Auto)
×
646
    {
647
      // Auto: try each viable HW decoder in priority order until one succeeds
648
      auto hwFmts = ::Video::selectHardwareAccelerations(
×
649
          api, codecPar->codec_id, vendorId);
×
650
      for(auto hwFmt : hwFmts)
×
651
      {
652
        hw_ok = setupHardwareDecoder(codec, hwFmt);
×
653
        if(hw_ok)
×
654
        {
655
          qDebug() << "DirectVideoNodeRenderer: using HW decoder"
×
656
                   << ((const AVCodec*)m_codec)->name
×
657
                   << "format:" << av_get_pix_fmt_name(m_hwPixelFormat);
×
658
          m_frameFormat.pixel_format = m_hwPixelFormat;
×
659
          break;
×
660
        }
661
      }
662
    }
×
663
    else
664
    {
665
      // User explicitly selected a decoder — map setting to pixel format
666
      AVPixelFormat hwFmt = AV_PIX_FMT_NONE;
×
667
      if(hwSetting == decoders.CUDA)
×
668
        hwFmt = AV_PIX_FMT_CUDA;
×
669
      else if(hwSetting == decoders.QSV)
×
670
        hwFmt = AV_PIX_FMT_QSV;
×
671
      else if(hwSetting == decoders.VDPAU)
×
672
        hwFmt = AV_PIX_FMT_VDPAU;
×
673
      else if(hwSetting == decoders.VAAPI)
×
674
        hwFmt = AV_PIX_FMT_VAAPI;
×
675
      else if(hwSetting == decoders.D3D)
×
676
        hwFmt = AV_PIX_FMT_D3D11;
×
677
#if defined(_WIN32) && LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(58, 29, 100)
678
      else if(hwSetting == decoders.D3D12)
679
        hwFmt = AV_PIX_FMT_D3D12;
680
#endif
681
      else if(hwSetting == decoders.DXVA)
×
682
        hwFmt = AV_PIX_FMT_DXVA2_VLD;
×
683
      else if(hwSetting == decoders.VideoToolbox)
×
684
        hwFmt = AV_PIX_FMT_VIDEOTOOLBOX;
×
685
      else if(hwSetting == decoders.V4L2)
×
686
        hwFmt = AV_PIX_FMT_DRM_PRIME;
×
687
      else if(hwSetting == decoders.VulkanVideo)
×
688
        hwFmt = AV_PIX_FMT_VULKAN;
×
689

690
      // Verify the chosen format is available and supports the codec
691
      if(hwFmt != AV_PIX_FMT_NONE
×
692
         && (!::Video::hardwareDecoderIsAvailable(hwFmt)
×
693
             || !::Video::codecSupportsHWPixelFormat(codecPar->codec_id, hwFmt, vendorId)))
×
694
      {
695
        hwFmt = AV_PIX_FMT_NONE;
×
696
      }
×
697

698
      if(hwFmt != AV_PIX_FMT_NONE)
×
699
      {
700
        hw_ok = setupHardwareDecoder(codec, hwFmt);
×
701
        if(hw_ok)
×
702
          m_frameFormat.pixel_format = m_hwPixelFormat;
×
703
      }
×
704
    }
705
  }
×
706
#endif
707

708
  // Fallback: software decode
709
  if(!hw_ok)
×
710
  {
711
    // setupHardwareDecoder may have freed m_codecContext — recreate it
712
    if(!m_codecContext)
×
713
    {
714
      m_codecContext = avcodec_alloc_context3(codec);
×
715
      avcodec_parameters_to_context(m_codecContext, codecPar);
×
716
      m_codecContext->pkt_timebase = m_avstream->time_base;
×
717
      m_codecContext->thread_count = 1;
×
718
    }
×
719

720
    int err = avcodec_open2(m_codecContext, codec, nullptr);
×
721
    if(err < 0)
×
722
    {
723
      avcodec_free_context(&m_codecContext);
×
724
      closeFile();
×
725
      return false;
×
726
    }
727
  }
×
728

729
  // Update timing from codec context
730
  auto tb = m_codecContext->pkt_timebase;
×
731
  m_dts_per_flicks = (tb.den / (tb.num * ossia::flicks_per_second<double>));
×
732
  m_flicks_per_dts = (tb.num * ossia::flicks_per_second<double>) / tb.den;
×
733

734
  // Allocate the reusable frame
735
  m_decodedFrame = av_frame_alloc();
×
736

737
  return true;
×
738
}
×
739

740
void DirectVideoNodeRenderer::closeFile()
×
741
{
742
  if(m_swTransferFrame)
×
743
  {
744
    av_frame_free(&m_swTransferFrame);
×
745
    m_swTransferFrame = nullptr;
×
746
  }
×
747

748
  if(m_decodedFrame)
×
749
  {
750
    av_frame_free(&m_decodedFrame);
×
751
    m_decodedFrame = nullptr;
×
752
  }
×
753

754
  if(m_codecContext)
×
755
  {
756
    avcodec_flush_buffers(m_codecContext);
×
757
    avcodec_free_context(&m_codecContext);
×
758
    m_codecContext = nullptr;
×
759
    m_codec = nullptr;
×
760
  }
×
761

762
  if(m_hwDeviceCtx)
×
763
  {
764
    av_buffer_unref(&m_hwDeviceCtx);
×
765
    m_hwDeviceCtx = nullptr;
×
766
  }
×
767

768
  if(m_formatContext)
×
769
  {
770
    avio_flush(m_formatContext->pb);
×
771
    avformat_flush(m_formatContext);
×
772
    avformat_close_input(&m_formatContext);
×
773
    m_formatContext = nullptr;
×
774
  }
×
NEW
775
  if(m_ioOwner)
×
776
  {
NEW
777
    m_ioFree(m_ioOwner);
×
NEW
778
    m_ioOwner = nullptr;
×
NEW
779
    m_ioFree = nullptr;
×
NEW
780
  }
×
781

782
  m_avstream = nullptr;
×
783
  m_hwPixelFormat = AV_PIX_FMT_NONE;
×
784
}
×
785

786
// ============================================================
787
//  Packet reading / decoding
788
// ============================================================
789

790
// Check if we can just read the next packet sequentially instead of seeking.
791
// For forward playback at normal speed, this avoids the expensive
792
// flush/seek/flush cycle on every frame.
793
bool DirectVideoNodeRenderer::isSequentialRead(int64_t flicks) const
×
794
{
795
  if(m_lastDecodedDts == INT64_MIN)
×
796
    return false; // No previous frame — must seek
×
797

798
  const double fps = m_fps > 0. ? m_fps : 24.;
×
799
  const int64_t frameDurationFlicks
×
800
      = static_cast<int64_t>(ossia::flicks_per_second<double> / fps);
×
801

802
  const int64_t lastFlicks
×
803
      = static_cast<int64_t>(m_lastDecodedDts * m_flicks_per_dts);
×
804
  const int64_t delta = flicks - lastFlicks;
×
805

806
  // Sequential if we're moving forward by 0–2 frames
807
  return delta >= 0 && delta <= frameDurationFlicks * 2;
×
808
}
×
809

810
bool DirectVideoNodeRenderer::readNextPacketRaw()
×
811
{
812
  auto packet = av_packet_alloc();
×
813
  bool found = false;
×
814
  int attempts = 0;
×
815

816
  while(av_read_frame(m_formatContext, packet) >= 0 && attempts < 10)
×
817
  {
818
    attempts++;
×
819
    if(packet->stream_index == m_avstream->index)
×
820
    {
821
      auto cp = m_avstream->codecpar;
×
822
      if(!m_decodedFrame)
×
823
        m_decodedFrame = av_frame_alloc();
×
824

825
      if(m_decodedFrame->buf[0])
×
826
        av_buffer_unref(&m_decodedFrame->buf[0]);
×
827

828
      m_decodedFrame->buf[0] = av_buffer_ref(packet->buf);
×
829
      m_decodedFrame->width = cp->width;
×
830
      m_decodedFrame->height = cp->height;
×
831
      m_decodedFrame->format = cp->codec_tag;
×
832
      m_decodedFrame->data[0] = packet->data;
×
833
      m_decodedFrame->linesize[0] = packet->size;
×
834
      m_decodedFrame->pts = packet->pts;
×
835
      m_decodedFrame->pkt_dts = packet->dts;
×
836

837
      m_lastDecodedDts = packet->dts;
×
838
      found = true;
×
839
      av_packet_unref(packet);
×
840
      break;
×
841
    }
842
    av_packet_unref(packet);
×
843
  }
844

845
  av_packet_free(&packet);
×
846
  return found;
×
847
}
848

849
bool DirectVideoNodeRenderer::readNextPacketAVCodec()
×
850
{
851
  av_frame_unref(m_decodedFrame);
×
852

853
  auto packet = av_packet_alloc();
×
854
  bool found = false;
×
855
  int attempts = 0;
×
856

857
  while(av_read_frame(m_formatContext, packet) >= 0 && attempts < 64)
×
858
  {
859
    if(packet->stream_index != m_avstream->index)
×
860
    {
861
      av_packet_unref(packet);
×
862
      continue;
×
863
    }
864

865
    attempts++;
×
866
    int ret = avcodec_send_packet(m_codecContext, packet);
×
867
    av_packet_unref(packet);
×
868
    if(ret < 0 && ret != AVERROR(EAGAIN))
×
869
      break;
×
870

871
    ret = avcodec_receive_frame(m_codecContext, m_decodedFrame);
×
872
    if(ret == 0)
×
873
    {
874
      m_lastDecodedDts = m_decodedFrame->pkt_dts;
×
875

876
      // Note: do NOT update m_frameFormat here. The format change detection
877
      // in update() compares the decoded frame format against m_frameFormat
878
      // to know when to rebuild the GPU decoder. Updating it here would hide
879
      // HW→SW fallback transitions (e.g. VideoToolbox rejecting a codec profile).
880

881
      found = true;
×
882
      break;
×
883
    }
884
    else if(ret != AVERROR(EAGAIN))
×
885
    {
886
      break;
×
887
    }
888
  }
889

890
  av_packet_free(&packet);
×
891
  return found;
×
892
}
893

894
bool DirectVideoNodeRenderer::seekAndDecode(int64_t flicks)
×
895
{
896
  if(!m_formatContext || !m_avstream)
×
897
    return false;
×
898

899
  if(flicks < 0)
×
900
    flicks = 0;
×
901

902
  // For sequential forward playback, skip the expensive seek
903
  const bool sequential = isSequentialRead(flicks);
×
904

905
  if(!m_useAVCodec)
×
906
  {
907
    // Raw GPU-compressed path (HAP, DXV)
908
    if(!sequential)
×
909
    {
910
      if(!ossia::seek_to_flick(m_formatContext, nullptr, m_avstream, flicks,
×
911
                               AVSEEK_FLAG_BACKWARD))
912
        return false;
×
913
    }
×
914
    return readNextPacketRaw();
×
915
  }
916

917
  // AVCodec path (ProRes, MJPEG, DNxHD, H.264, HEVC, etc.)
918
  if(!m_codecContext || !m_decodedFrame)
×
919
    return false;
×
920

921
  if(!sequential)
×
922
  {
923
    av_frame_unref(m_decodedFrame);
×
924
    if(!ossia::seek_to_flick(m_formatContext, m_codecContext, m_avstream, flicks,
×
925
                             AVSEEK_FLAG_BACKWARD))
926
      return false;
×
927
  }
×
928

929
  return readNextPacketAVCodec();
×
930
}
×
931

932
// ============================================================
933
//  GPU decoder creation
934
// ============================================================
935

936
PixelFormatInfo DirectVideoNodeRenderer::hwPixelFormatInfo() const
×
937
{
938
  auto codecparFmt = m_avstream
×
939
      ? static_cast<AVPixelFormat>(m_avstream->codecpar->format)
×
940
      : AV_PIX_FMT_NONE;
941
  int bitsPerRaw = m_avstream ? m_avstream->codecpar->bits_per_raw_sample : 0;
×
942
  return PixelFormatInfo::fromCodecParameters(m_hwSwFormat, codecparFmt, bitsPerRaw);
×
943
}
944

945
std::unique_ptr<GPUVideoDecoder>
946
DirectVideoNodeRenderer::tryCreateZeroCopyDecoder(QRhi& rhi)
×
947
{
948
#if LIBAVUTIL_VERSION_MAJOR >= 57
949
  switch(m_hwPixelFormat)
×
950
  {
951
#if defined(__linux__)
952
    case AV_PIX_FMT_VAAPI:
953
    {
954
#if QT_HAS_VULKAN && defined(SCORE_HAS_DRM_HWCONTEXT) \
955
    && defined(VK_EXT_image_drm_format_modifier) && defined(VK_KHR_external_memory_fd) \
956
    && QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
957
      if(m_rhiApi == GraphicsApi::Vulkan
958
         && HWVaapiVulkanDecoder::isAvailable(rhi))
959
      {
960
        return std::make_unique<HWVaapiVulkanDecoder>(
961
            m_frameFormat, rhi, hwPixelFormatInfo());
962
      }
963
#endif
964
      break;
×
965
    }
966
#endif // __linux__
967
    case AV_PIX_FMT_VULKAN:
968
    {
969
      // Shared device: GPU plane copy from multiplane to separate VkImages
970
#if defined(SCORE_HAS_VULKAN_HWCONTEXT_SHARED) && QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
971
      if(m_sharedVulkanDevice
972
         && m_rhiApi == GraphicsApi::Vulkan
973
         && HWVulkanSharedDecoder::isAvailable(rhi))
974
      {
975
        // qDebug() << "DirectVideoNodeRenderer: zero-copy: HWVulkanSharedDecoder"
976
        //          << "sw_format:" << av_get_pix_fmt_name(m_hwSwFormat);
977
        return std::make_unique<HWVulkanSharedDecoder>(
978
            m_frameFormat, rhi, hwPixelFormatInfo());
979
      }
980
#endif
981

982
      // DMA-BUF bridge fallback disabled — it doesn't work on NVIDIA
983
      // (can't export Vulkan Video decoded frames as DMA-BUF) and causes
984
      // green screen when it fails. Fall through to HWTransferDecoder
985
      // which does GPU decode + CPU transfer (still faster than software).
986
      break;
×
987
    }
988
    case AV_PIX_FMT_CUDA:
989
    {
990
#if defined(SCORE_HAS_CUDA_HWCONTEXT) && QT_HAS_VULKAN \
991
    && QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
992
      if(m_rhiApi == GraphicsApi::Vulkan
993
         && HWCudaVulkanDecoder::isAvailable(rhi, m_hwDeviceCtx))
994
      {
995
        return std::make_unique<HWCudaVulkanDecoder>(
996
            m_frameFormat, rhi, m_hwDeviceCtx, hwPixelFormatInfo());
997
      }
998
#endif
999
      break;
×
1000
    }
1001
#if defined(_WIN32)
1002
    case AV_PIX_FMT_D3D11:
1003
    {
1004
#if defined(SCORE_HAS_D3D11_HWCONTEXT)
1005
      if(m_rhiApi == GraphicsApi::D3D11
1006
         && HWD3D11Decoder::isAvailable(rhi))
1007
      {
1008
        return std::make_unique<HWD3D11Decoder>(
1009
            m_frameFormat, rhi, hwPixelFormatInfo());
1010
      }
1011
#endif
1012
      break;
1013
    }
1014
#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(58, 29, 100)
1015
    case AV_PIX_FMT_D3D12:
1016
    {
1017
#if defined(SCORE_HAS_D3D12_HWCONTEXT)
1018
      if(m_rhiApi == GraphicsApi::D3D12
1019
         && HWD3D12Decoder::isAvailable(rhi))
1020
      {
1021
        return std::make_unique<HWD3D12Decoder>(
1022
            m_frameFormat, rhi, hwPixelFormatInfo());
1023
      }
1024
#endif
1025
      break;
1026
    }
1027
#endif
1028
#endif
1029
#if defined(__APPLE__)
1030
    case AV_PIX_FMT_VIDEOTOOLBOX:
1031
    {
1032
#if defined(SCORE_HAS_VTB_HWCONTEXT)
1033
      if(m_rhiApi == GraphicsApi::Metal
1034
         && HWVideoToolboxDecoder::isAvailable(rhi))
1035
      {
1036
        auto codecparFmt = m_avstream
1037
            ? static_cast<AVPixelFormat>(m_avstream->codecpar->format)
1038
            : AV_PIX_FMT_NONE;
1039
        int bitsPerRaw = m_avstream
1040
            ? m_avstream->codecpar->bits_per_raw_sample : 0;
1041
        auto fmtInfo = PixelFormatInfo::fromCodecParameters(
1042
            m_hwSwFormat, codecparFmt, bitsPerRaw);
1043
        return std::make_unique<HWVideoToolboxDecoder>(
1044
            m_frameFormat, rhi, fmtInfo);
1045
      }
1046
#endif
1047
      break;
1048
    }
1049
#endif
1050
    default:
1051
      break;
×
1052
  }
1053
#endif // LIBAVUTIL_VERSION_MAJOR >= 57
1054
  return nullptr;
×
1055
}
1056

1057
void DirectVideoNodeRenderer::createGpuDecoder(QRhi& rhi)
×
1058
{
1059
#if LIBAVUTIL_VERSION_MAJOR >= 57
1060
  // If hardware acceleration is active, try zero-copy first,
1061
  // then fall back to HWTransferDecoder (GPU decode + DMA transfer).
1062
  if(m_hwPixelFormat != AV_PIX_FMT_NONE && m_hwDeviceCtx)
×
1063
  {
1064
    if(!m_zeroCopyFailed)
×
1065
    {
1066
      auto zc = tryCreateZeroCopyDecoder(rhi);
×
1067
      if(zc)
×
1068
      {
1069
        m_gpu = std::move(zc);
×
1070
        m_recomputeScale = true;
×
1071
        return;
×
1072
      }
1073
    }
×
1074

1075
    // Zero-copy not available — use transfer decoder
1076
    // (GPU decode → DMA transfer to CPU → normal texture upload)
1077
    AVPixelFormat swFmt = m_hwSwFormat != AV_PIX_FMT_NONE
×
1078
                              ? m_hwSwFormat
×
1079
                              : AV_PIX_FMT_NV12;
1080
    m_gpu = std::make_unique<HWTransferDecoder>(m_frameFormat, swFmt);
×
1081
    // qDebug() << "DirectVideoNodeRenderer: using HW transfer decoder, sw_format:"
1082
    //          << av_get_pix_fmt_name(swFmt);
1083
    m_recomputeScale = true;
×
1084
    return;
×
1085
  }
1086
#endif
1087

1088
  auto& model = const_cast<VideoNodeBase&>(node());
×
1089
  auto& filter = model.m_filter;
×
1090

1091
  m_gpu = createGPUVideoDecoder(m_frameFormat, filter.toStdString());
×
1092
  if(m_gpu)
×
1093
  {
1094
    // qDebug() << "DirectVideoNodeRenderer: using SW decoder for"
1095
    //          << av_get_pix_fmt_name(m_frameFormat.pixel_format);
1096
  }
×
1097
  else
1098
  {
1099
    qDebug() << "DirectVideoNodeRenderer: Unhandled pixel format: '"
×
1100
             << av_get_pix_fmt_name(m_frameFormat.pixel_format) << "'";
×
1101
    m_gpu = std::make_unique<EmptyDecoder>();
×
1102
  }
1103

1104
  m_recomputeScale = true;
×
1105
}
×
1106

1107
// ============================================================
1108
//  Pipeline management
1109
// ============================================================
1110

1111
void DirectVideoNodeRenderer::setupGpuDecoder(RenderList& r)
×
1112
{
1113
  if(m_gpu)
×
1114
  {
1115
    m_gpu->release(r);
×
1116
    for(auto& p : m_p)
×
1117
      p.second.release();
×
1118
    m_p.clear();
×
1119
  }
×
1120

1121
  createGpuDecoder(*r.state.rhi);
×
1122
  createPipelines(r);
×
1123
}
×
1124

1125
void DirectVideoNodeRenderer::createPipelines(RenderList& r)
×
1126
{
1127
  if(m_gpu)
×
1128
  {
1129
    auto shaders = m_gpu->init(r);
×
1130
    SCORE_ASSERT(m_p.empty());
×
1131
    score::gfx::defaultPassesInit(
×
1132
        m_p, this->node().output[0]->edges, r, r.defaultQuad(), shaders.first,
×
1133
        shaders.second, m_processUBO, m_materialUBO, m_gpu->samplers);
×
1134
  }
×
1135
}
×
1136

1137
void DirectVideoNodeRenderer::init(RenderList& renderer, QRhiResourceUpdateBatch& res)
×
1138
{
1139
  auto& rhi = *renderer.state.rhi;
×
1140

1141
  const auto& mesh = renderer.defaultQuad();
×
1142
  if(m_meshBuffer.buffers.empty())
×
1143
  {
1144
    m_meshBuffer = renderer.initMeshBuffer(mesh, res);
×
1145
  }
×
1146

1147
  m_processUBO = rhi.newBuffer(
×
1148
      QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(ProcessUBO));
×
1149
  m_processUBO->setName("DirectVideoNodeRenderer::m_processUBO");
×
1150
  m_processUBO->create();
×
1151

1152
  m_materialUBO
×
1153
      = rhi.newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(Material));
×
1154
  m_materialUBO->setName("DirectVideoNodeRenderer::m_materialUBO");
×
1155
  m_materialUBO->create();
×
1156

1157
  // Open our own decode context, passing the RHI API for HW accel selection
1158
  if(!openFile(renderer.state.api, renderer.state.rhi))
×
1159
  {
1160
    qDebug() << "DirectVideoNodeRenderer: failed to open" << m_filePath.c_str();
×
1161
  }
×
1162

1163
  createGpuDecoder(rhi);
×
1164
  createPipelines(renderer);
×
1165
  m_recomputeScale = true;
×
1166
}
×
1167

1168
// ============================================================
1169
//  Render pass
1170
// ============================================================
1171

1172
void DirectVideoNodeRenderer::runRenderPass(
×
1173
    RenderList& renderer, QRhiCommandBuffer& cb, Edge& edge)
1174
{
1175
  if(!m_gpu || !m_gpu->hasFrame)
×
1176
    return;
×
1177
  score::gfx::quadRenderPass(renderer, m_meshBuffer, cb, edge, m_p);
×
1178
}
×
1179

1180
void DirectVideoNodeRenderer::update(
×
1181
    RenderList& renderer, QRhiResourceUpdateBatch& res, Edge* edge)
1182
{
1183
  res.updateDynamicBuffer(
×
1184
      m_processUBO, 0, sizeof(ProcessUBO), &this->node().standardUBO);
×
1185

1186
  // Compute desired time in flicks
1187
  const double currentTime = this->node().standardUBO.time;
×
1188
  const int64_t currentFlicks
×
1189
      = std::max(int64_t{0}, static_cast<int64_t>(currentTime * ossia::flicks_per_second<double>));
×
1190

1191
  // Only re-decode if we moved to a different time
1192
  if(currentFlicks != m_lastRequestedFlicks)
×
1193
  {
1194
    // Frame duration in flicks
1195
    const double fps = m_fps > 0. ? m_fps : 24.;
×
1196
    const int64_t frameDurationFlicks
×
1197
        = static_cast<int64_t>(ossia::flicks_per_second<double> / fps);
×
1198

1199
    // Skip decode if we already have this frame (within ±1 frame of the same position)
1200
    const int64_t lastDecodedFlicks
×
1201
        = m_lastDecodedDts == INT64_MIN
×
1202
            ? INT64_MIN
1203
            : static_cast<int64_t>(m_lastDecodedDts * m_flicks_per_dts);
×
1204

1205
    m_lastRequestedFlicks = currentFlicks;
×
1206

1207
    if(m_lastDecodedDts == INT64_MIN
×
1208
       || std::abs(currentFlicks - lastDecodedFlicks) >= frameDurationFlicks)
×
1209
    {
1210
      // HW frames may store surface handles in data[3] (QSV, VAAPI)
1211
      // instead of data[0], so check format instead of data pointer.
1212
      if(seekAndDecode(currentFlicks) && m_decodedFrame
×
1213
         && (m_decodedFrame->data[0]
×
1214
             || Video::formatIsHardwareDecoded(
×
1215
                 static_cast<AVPixelFormat>(m_decodedFrame->format))))
×
1216
      {
1217
        // Check if format changed (e.g. resolution change)
1218
        if(m_gpu && m_useAVCodec)
×
1219
        {
1220
          const auto& n = this->node();
×
1221
          auto fmt = static_cast<AVPixelFormat>(m_decodedFrame->format);
×
1222

1223
          // For HW decode: the actual sw_format is only known after
1224
          // the first decode when hw_frames_ctx is created. Check if it
1225
          // differs from what we assumed at init time.
1226
#if LIBAVUTIL_VERSION_MAJOR >= 57
1227
          if(Video::formatIsHardwareDecoded(fmt) && m_codecContext
×
1228
             && m_codecContext->hw_frames_ctx && !m_hwSwFormatChecked)
×
1229
          {
1230
            m_hwSwFormatChecked = true;
×
1231
            auto* fc = reinterpret_cast<AVHWFramesContext*>(
×
1232
                m_codecContext->hw_frames_ctx->data);
×
1233
            auto realSwFmt = static_cast<AVPixelFormat>(fc->sw_format);
×
1234
            if(realSwFmt != m_hwSwFormat)
×
1235
            {
1236
              m_hwSwFormat = realSwFmt;
×
1237
              setupGpuDecoder(renderer);
×
1238
            }
×
1239
          }
×
1240
#endif
1241

1242
          if(fmt != m_frameFormat.pixel_format
×
1243
             || m_decodedFrame->width != m_frameFormat.width
×
1244
             || m_decodedFrame->height != m_frameFormat.height
×
1245
             || n.m_outputFormat != m_frameFormat.output_format
×
1246
             || n.m_tonemap != m_frameFormat.tonemap)
×
1247
          {
1248
            // Detect HW→SW fallback: we expected a HW format but got a SW one.
1249
            // This happens when the HW decoder rejects the codec profile
1250
            // (e.g. VideoToolbox can't handle certain ProRes profiles).
1251
            // Clear HW state so createGpuDecoder() uses the SW decoder path.
1252
            if(m_hwPixelFormat != AV_PIX_FMT_NONE
×
1253
               && !Video::formatIsHardwareDecoded(fmt))
×
1254
            {
1255
              m_hwPixelFormat = AV_PIX_FMT_NONE;
×
1256
            }
×
1257

1258
            m_frameFormat.pixel_format = fmt;
×
1259
            m_frameFormat.width = m_decodedFrame->width;
×
1260
            m_frameFormat.height = m_decodedFrame->height;
×
1261
            m_frameFormat.output_format = n.m_outputFormat;
×
1262
            m_frameFormat.tonemap = n.m_tonemap;
×
1263
            setupGpuDecoder(renderer);
×
1264
          }
×
1265
        }
×
1266

1267
        if(m_gpu)
×
1268
        {
1269
          m_gpu->exec(renderer, res, *m_decodedFrame);
×
1270
          m_gpu->hasFrame = true;
×
1271

1272
          // If the GPU decoder flagged failure (e.g. incompatible VTB
1273
          // pixel format, CVMetalTextureCache error), fall back to
1274
          // HWTransferDecoder (GPU decode + CPU transfer + SW upload).
1275
          if(m_gpu->failed)
×
1276
          {
1277
            // Prevent tryCreateZeroCopyDecoder from being tried again,
1278
            // but keep m_hwDeviceCtx alive for HWTransferDecoder.
1279
            m_zeroCopyFailed = true;
×
1280
            setupGpuDecoder(renderer);
×
1281
          }
×
1282
        }
×
1283
      }
×
1284
    }
×
1285
  }
×
1286

1287
  if(m_recomputeScale || m_currentScaleMode != this->node().m_scaleMode)
×
1288
  {
1289
    m_currentScaleMode = this->node().m_scaleMode;
×
1290
    auto sz = computeScaleForMeshSizing(
×
1291
        m_currentScaleMode, renderer.renderSize(edge),
×
1292
        QSizeF(m_frameFormat.width, m_frameFormat.height));
×
1293
    Material mat;
×
1294
    mat.scale_w = sz.width();
×
1295
    mat.scale_h = sz.height();
×
1296
    mat.tex_w = m_frameFormat.width;
×
1297
    mat.tex_h = m_frameFormat.height;
×
1298

1299
    res.updateDynamicBuffer(m_materialUBO, 0, sizeof(Material), &mat);
×
1300
    m_recomputeScale = false;
×
1301
  }
×
1302
}
×
1303

1304
void DirectVideoNodeRenderer::release(RenderList& r)
×
1305
{
1306
  // Destroy GPU decoder BEFORE closeFile() frees m_hwDeviceCtx.
1307
  // HW decoders (CUDA, Vulkan) hold references to the HW device context
1308
  // and must be destroyed while it's still valid.
1309
  if(m_gpu)
×
1310
  {
1311
    m_gpu->release(r);
×
1312
    m_gpu.reset();
×
1313
  }
×
1314

1315
  delete m_processUBO;
×
1316
  m_processUBO = nullptr;
×
1317

1318
  delete m_materialUBO;
×
1319
  m_materialUBO = nullptr;
×
1320

1321
  for(auto& p : m_p)
×
1322
    p.second.release();
×
1323
  m_p.clear();
×
1324

1325
  m_meshBuffer = {};
×
1326

1327
  closeFile();
×
1328
}
×
1329

1330
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc