Skip to content

split/gpu interop#2078

Closed
jcelerier wants to merge 156 commits into
masterfrom
split/gpu-interop
Closed

split/gpu interop#2078
jcelerier wants to merge 156 commits into
masterfrom
split/gpu-interop

Conversation

@jcelerier

Copy link
Copy Markdown
Member
  • isf: TEX_DIMENSIONS_3D / IMG_SIZE_3D aliases for 3D samplers
  • isf: point3d_input AS_COLOR flag for color-swatch display
  • isf: parse-time warning on unknown csf_image_input FORMAT
  • isf: parser rework — top-level descriptors, uniform inputs, geometry AUXILIARY, vertex inputs
  • 3rdparty: add OffsetAllocator submodule
  • gfx: add AssetTable + TextureLoader for shared decoded-asset cache
  • gfx: add GpuResourceRegistry — slab-allocated GPU arenas backed by OffsetAllocator
  • gfx: add CameraMath / GpuTiming / VertexFallback helpers
  • gfx: add IsfBindingsBuilder + PipelineStateHelpers
  • gfx: add SceneGPUState — FlatScene + scene packer
  • gfx: add OffscreenDevice + RhiPreviewWidget + Metal buffer-copy backend
  • gfx: extend ISF / SimpleRenderedISF nodes for new ISF features
  • csf: rework for scene-aware compute pipeline
  • gfx: rework raw raster pipeline (MRT, AUXILIARY, EXECUTION_MODEL) and VSA
  • gfx: refresh shared graph plumbing for incremental scene pipeline
  • gfx: add ScenePreprocessorNode — scene_spec to flat draws + arena uploads
  • gfx: add SceneFilterNode + FlattenedSceneFilterNode + MergeGeometriesNode
  • avnd: split CpuFilter / CpuAnalysis lifecycle into init / initState / release
  • avnd: add scene_port concept
  • avnd: split GpuNode lifecycle and add scene_port storage helpers
  • gfx: extend ShaderProgram for new ISF features
  • gfx: refresh Filter / GeometryFilter / TexturePort for new pipeline
  • gfx: refresh WindowDevice / Spout / Syphon / WindowCapture
  • gfx: refresh GfxContext + window / screen / multiwindow nodes for incremental scene rebuild
  • js: gpu node lifecycle rework with deterministic teardown
  • wip: many bugfixes across the board
  • build: more build fixes
  • ci: fixes for older qt versions
  • rhi: populate caps
  • ci: try windows fixes
  • gfx: restore PhongNode, still used by score-vfx-template
  • gfx: keep TextureRenderTarget compatible with addons' aggregate init
  • gfx: do not include mmsystem.h in CommonUBOs.hpp
  • gfx: fix build with Qt < 6.9
  • gfx: fix designator order after TextureRenderTarget reorder
  • gfx: fix build with Qt 6.4
  • gfx: more Qt 6.4 compatibility guards
  • xcb: use EGL instead of GLX by default
  • desktop: enforce desktop GL (Qt may pick GLES when we use a EGL surface)
  • gpu: try to land a generalized abstraction for interop & RDMA

@jcelerier jcelerier force-pushed the split/gpu-interop branch 3 times, most recently from 851f361 to b1c6d32 Compare June 26, 2026 03:15
jcelerier added 27 commits July 6, 2026 21:02
…AUXILIARY, vertex inputs

Top-level descriptors (PIPELINE_STATE, MULTIVIEW, EXECUTION_MODEL, EXTENSIONS,
CLIP_DISTANCES / CULL_DISTANCES, DEPTH_LAYOUT). New input kinds: uniform_input
(UBO INPUTS), per-input sampler_config (WRAP / FILTER / COMPARE / MIPS),
audio_sampler_config. Storage / image / cubemap input extensions: PERSISTENT,
IS_ARRAY, GENERATE_MIPS, cube/3D textures. Geometry input AUXILIARY: storage +
sampled, persistent, is_uniform, depth companion, INDIRECT block. Per-pass:
LAYER, Z, FORMAT, PIPELINE_STATE override. OUTPUTS: LAYERS, DEPTH, FORMAT,
SAMPLES, CUBEMAP, GENERATE_MIPS, WIDTH/HEIGHT expressions. Vertex inputs:
REQUIRED, DEFAULT, SEMANTIC, INTERPOLATION (with auto-flat for int/bool
varyings).

Bug fixes: mat3/mat4 attribute slot count, TYPES struct emission in BOTH
stages, drop incorrect std430 padding heuristic that was breaking RawLight
stride.
jcelerier and others added 26 commits July 11, 2026 21:59
…oundtrip

- New per-cell `trans`/`wire` columns backed by the device testability
  accessors: a dmabuf-requested cell that runs on the readback fallback
  reports `dma!fb -> shm` and PASS(fallback) instead of masquerading as
  zero-copy; the transport is sampled before stop() (which resets it).
- Vulkan DMA-BUF output cells enabled (were GL-only) — this is the
  configuration that proves true zero-copy on Qt >= 6.6.
- Match production startup: QT_XCB_GL_INTEGRATION=xcb_egl on Linux
  (src/app/main.cpp does the same; MinimalGUIApplication bypasses it),
  respecting a pre-set environment override.
- SCORE_PWRT_DUMP=<prefix> dumps the first verified frame per cell for
  visual diffing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…vendor pin

nvidia_p2p_get_pages only pins CUDA-allocator memory; pinning CUDA-imported
GL buffers can never work. Give the vendor a CUDA-owned bounce buffer per
slot (cuda_p2p_alloc_buffer + SYNC_MEMOPS) and bridge to the GL storage ring
with one on-GPU DtoD copy per frame — frames never touch sysmem.

Because pin return codes lie on IOMMU-translated platforms (writes silently
drop), VendorDmaRegistrar grows an optional verifyTransfer content probe;
GpuDirectOutput runs it at init and falls back honestly.

Also: GL register-only mode in GpuRingBuffer for the capture direction, and
a teardown-order fix in DMACaptureInputNode (release the strategy — which
unpins through the card — before destroying the backend that owns the card).

Validated end-to-end on 2x Kona 5 + Quadro RTX 4000 (iommu=pt): RDMA-GL/T3
engages both directions, 3-firmware sweep, 44-52 dB, ~87 ms vs ~150 ms CPU.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
A factory returning nullptr (missing hardware, headless environment) put a
null unique_ptr in ApplicationComponents::panels, crashing every later
panels() iteration (e.g. Explorer::findDeviceExplorerWidgetInstance from
Scenario::SearchWidget). Warn and skip instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…null

Same defect as registerPanel: a declining PanelDelegateFactory put a null
unique_ptr in ApplicationComponents::panels and setModel dereferenced it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
ctx.panels()'s indirect iteration dereferences every slot unconditionally,
which aborts on hardened stdlibs (_GLIBCXX_ASSERTIONS) when a slot is empty;
findPanel<T>() does the same lookup null-safely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…ownload

The first-run library-download question is modal; in non-interactive
sessions (tests/CI) it blocks the main thread forever. Same escape hatch
refresh() already uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…pplication

The test scaffolding deletes the QApplication in the destructor BODY while
score::Settings is a value member destroyed after it — its QObject settings
models then hit freed application state (SEGV in QObject::~QObject on
Qt 6.11; silently tolerated on older Qt). Settings grows an idempotent
teardownModels() that both destructors invoke before deleting the app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
staticVulkanInstance() set the invalid flag on create() failure but still
returned the pointer to the first caller — Graph's Vulkan->GL fallback
check passed and QRhi::create crashed on the dead instance (hit on any
platform where the QPA lacks Vulkan, e.g. QT_QPA_PLATFORM=offscreen).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…t current

dvpCloseGLContext poisons the GL context it was bound to. When init failed
AFTER binding DVP (e.g. no fragment encoder for the wire format), release()
closed DVP and the caller kept rendering on the same context — SEGV in
QRhi::endOffscreenFrame a few frames later. Validate encoder/sizes/native
handles before touching DVP so failure paths have no side effects, and make
the owning context current in release() (required by DVP, previously only
accidentally true).

Repro: AJARoundtrip --interop dvp with a DVP-capable cell followed by an
RGBA8 cell on the Quadro; 12-cell stress now exits with a full matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
- New R210Decoder (DeckLink bmdFormat10BitRGB SDI 4:4:4 wire), exact
  inverse of PackedRGBEncoder::r210be(); wired into makeWireDecoder.
- v210 encode+decode now size rows on the padded wire stride
  (((w+47)/48)*128 bytes) instead of a tight (w/6)*4 texel row: at 1280
  and 2048-DCI widths the old math truncated the tail group and skewed
  every row against the wire, collapsing roundtrip PSNR to ~31 dB.
  Compute encoder edge-clamps tail-group fetches (texelFetch does not).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
… sequence

Three findings from a standalone two-context GLX probe against libdvp 1.69:

- dvpInitGLContext(0) makes libdvp create its own internal GL context; only
  the FIRST init'd context in a process can transfer — a second one (output
  node + capture node, each with their own QRhi) gets DVP_STATUS_ERROR from
  every dvpMemcpyLined while setup calls all succeed. Init with
  DVP_DEVICE_FLAGS_SHARE_APP_CONTEXT (1) instead.
- Texture-destination copies follow Blender/UPBGE's shape: EndAPI before the
  DVP scope, buffer sync as CPU-signalled source acquire, a per-texture sync
  signalled on completion, WaitAPI after. Textures now get their own sync
  slot at registration.
- The upload runs on the render thread via the SPSC slot publisher (capture
  thread only publishes); the strategy allocates its own RenderTarget-flagged
  texture and hands it to the node via the strategy-owned texture protocol.

AJA roundtrip --interop dvp --rx gpu: DVP-GL engaged both directions,
1080p5994 YCbCr8/v210 PASS 51.28/51.92 dB, zero transfer errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
PacedFramePump waited one vendor tick before every submit, serialising
tick-period + DMA: throughput capped at 1/(VBI + transfer) — measured
46 fps at UHD60 and 24 fps at 8K on Kona 5 while every stage had head-
room. When the vendor provides canAccept (AJA AutoCirculate), stuff the
card's ring back-to-back and block on the tick only under back-pressure;
vendors with null canAccept (DeckLink free-pool, Bluefish sync,
Deltacast blocking submit) keep the one-tick-per-submit contract. Also
stop discarding an already-drained frame on a canAccept blip.

StageProfiler: SCORE_AJA_PROFILE=1 wall-time attribution per pipeline
stage (paint/upload/encoder-sync/DtoD/AC-transfer); instruments
GpuDirectOutput's sync + bounce copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
VulkanCudaBounce pairs, per slot, a Vulkan-owned exportable buffer
(CUDA-imported through the SUPPORTED interop direction) with a pinned
cuMemAlloc bounce the vendor DMAs; flushToBounce/flushFromBounce bridge
the two with one DtoD. Hard-won constraints, all measured on hardware:
- a CUDA-VMM fd imported INTO Vulkan allocates cleanly but does not
  alias — writes land in private memory (wrong interop direction);
- vkGetMemoryFdPropertiesKHR is spec-forbidden for OPAQUE_FD handles
  (VUID 00674) — the import helper now skips the query for opaque types;
- multi-GPU: the Vulkan physical device must be the CUDA device;
  createSharedVulkanDevice now honours QT_VK_PHYSICAL_DEVICE_INDEX
  (QRhi's own env) instead of hardcoding physDevs[0].
Also: GpuRingBuffer Vulkan plain mode (QRhi storage buffers + native
VkBuffer, no bridge import), cuda_p2p_copy_dtod_2d (pitched flat DtoD
for row-padded linear images), and CUDA VMM fd export plumbing
(cuMemExportToShareableHandle + CudaVmmAllocation::exportPosixFd,
probe-verified pinnable — kept as API surface for future seams).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Replace the finish()+finish() double drain in prepareNextFrame with GPU
ordering: the copy frame's pre-barrier covers all earlier submission-order
work (no host wait after encode), and an empty queue submit signals an
exported timeline semaphore that CUDA waits on-stream — the single host
block left is the final DtoD stream sync. 1080p5994 Vulkan rx-gpu latency:
105.8 -> 85.6 ms (GL parity), zero lost/repeated frames.

Gated on the device having been created WITH timelineSemaphore enabled:
score's shared-device path (Qt >= 6.6) enables every queried feature and
now reports it (SharedVulkanDevice::timelineSemaphores ->
vkinterop::setDeviceTimelineSemaphoresEnabled). QRhi-created devices
(Qt 6.4) never enable the feature — the validation layer confirmed using
it there is a spec violation the driver merely tolerates — so they keep
the finish() fallback. Queue-drain before semaphore destroy fixes the
teardown VUID.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…llapse (d)

Adds the CUDA driver-API surface for exporting a mapped device VA range as
a dma-buf fd — the cross-API handle type Vulkan/GL can import as an aliasing
image, i.e. the export half of AJA "capture-collapse" option (d) (make the
AutoCirculate DMA target itself the sampled resource, deleting the bounce).

- CudaFunctions: resolve cuMemGetHandleForAddressRange; add
  CU_MEM_RANGE_HANDLE_TYPE_DMA_BUF_FD + the PCIE mapping flag +
  CU_DEVICE_ATTRIBUTE_DMA_BUF_SUPPORTED; add dmaBufExportSupported(device).
- CudaVmmAllocation::exportDmaBufFd(): dma-buf export of the mapped range,
  distinct from exportPosixFd() (opaque fd, which does not alias into Vulkan).

Gated + honest: option (d) is NOT wired into the capture strategies. The
rdmaprobe4 spike showed CU_DEVICE_ATTRIBUTE_DMA_BUF_SUPPORTED = 0 on this
rig's Quadro RTX 4000 and GeForce RTX 4090 (driver 595.71.05), so
cuMemGetHandleForAddressRange(DMA_BUF_FD) returns CUDA_ERROR_NOT_SUPPORTED
for both VMM and legacy cuMemAlloc. The Quadro is the only GPU here with
GPUDIRECT_RDMA=1 (required for the AJA pin) yet cannot export a dma-buf, so
the both-pinnable-and-exportable requirement is unsatisfiable and the bounce
path is retained. This plumbing lets a future DMA_BUF-capable GPU wire up (d)
mechanically, gated on dmaBufExportSupported().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Adds the plumbing for AJA capture-collapse option (a) and the Vulkan
double-buffer correctness fix (see aja-capture-collapse-analysis §7).

CudaFunctions: dlopen two more libcuda driver-API symbols —
cuGraphicsGLRegisterImage + cuGraphicsSubResourceGetMappedArray (OPTIONAL,
null-checked, no CUDA Toolkit needed).

CudaP2PBridge: cuda_p2p_register_gl_image / cuda_p2p_gl_write_image — register
a GL texture and cuMemcpy2D a device bounce straight into its level-0 array,
map/unmap per frame for GL coherency (mirrors the register_gl_buffer /
gl_write_buffer pair, array dst instead of buffer dst).

GpuDirectCaptureStrategy: add currentTexture() (defaults to outputTexture())
so a double-buffering strategy can publish a fresh sampled texture per frame.

DMACaptureInputNode: after acquireForRender, rebind every pass's SRB to
currentTexture() when it changes (replaceTexture/updateResources — no pipeline
rebuild). No-op for single-texture strategies (constant currentTexture()).

Validated on the AJA rig (Kona5, Quadro RTX 4000 RDMA), connector pair 3:
GL RDMA-GL/T3 and Vulkan RDMA-Vulkan/T3 both PASS at 1080p5994 + UHD_SL_60,
YCbCr8 + v210, PSNR held at baseline (52.26 / 51.92 dB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…module

The open NVIDIA kernel module (595.71.05) unblocks the CUDA dma-buf EXPORT
half of capture-collapse option (d): DMA_BUF_SUPPORTED=1 on the Quadro,
cuMemGetHandleForAddressRange(DMA_BUF_FD) returns a valid fd, and
DMABufferLock(inRDMA=true) now succeeds directly on a VMM allocation.

But option (d) still cannot engage: the NVIDIA graphics-side IMPORT of the
CUDA-exported dma-buf is unsupported. Vulkan advertises no
VK_EXT_external_memory_dma_buf (only drm_format_modifier), and EGL rejects a
CUDA-VMM dma-buf with EGL_BAD_ALLOC. The CUDA-bounce path (OPAQUE_FD image +
separate DMABufferLock'd bounce + per-frame DtoD) is retained.

The doc comment on exportDmaBufFd() previously blamed the export returning
CUDA_ERROR_NOT_SUPPORTED; that is stale on the open module. Corrected to name
the real, import-side blocker. Verified end-to-end by scratchpad/rdmaprobe4
(Vulkan, fails at import) and rdmaprobe4gl (GL/EGL, fails at import); the
CUDA-export + AJA-pin + card->GPU DMA half content-verifies at 100%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…Frame

M1: hoist the verbatim-duplicated one-time UNDEFINED->GENERAL image layout
transition (copied into 3 vendor RDMA shims) into a single
score::gfx::vkinterop::transitionImageLayout() in VkExternalMemoryHelpers.
Conservative access/stage masks (superset of the shader-read capture and
transfer-write output uses) + the existing vkQueueWaitIdle keep it
behaviour-identical.

M5: hoist the BAR1 slot-count policy ("frame >= 32MB ? reduced : full"),
duplicated across the GL/Vulkan capture and output RDMA shims, into
interop::rdmaRingDepthForFrame() (new RdmaRingDepth.hpp).

Additive: old vendor code repointed onto the new generics; no rename yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Add score::gfx::interop::CpuStagedCapture<Policy>: the host-staged capture
strategy (3-slot page-locked sysmem ring + portable QRhi upload, optional
raw-GL fast path) that the five vendor CPU captures each re-implemented with
only the name/namespace differing. A Policy supplies the deltas
(has_dma_lock for AJA's DMABufferLock, has_gl_fast_path for AJA's raw
glTexSubImage2D, the log tag/env/name strings). Behaviour-identical to each
original: AJA keeps its tightly-packed texW*4 stride, the CPU-only vendors
keep their raster-pitch (frameByteSize/height) stride.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
The AJA and Deltacast Vulkan RDMA paths share only the raw-Vulkan-handle
acquisition out of QRhiVulkanNativeHandles + score's shared QVulkanInstance
(copied verbatim into 3 shims). Hoist it into
interop::acquireVulkanRhiContext (VulkanRhiContext.hpp).

Deeper M3/M4 unification (one transfer base for both vendors) is DEFERRED and
documented in the header: the two paths diverge by design — AJA uses per-slot
LINEAR images imported as flat CUDA buffers (copy dtod-2d) + an exportable
VkBuffer bounce pinned via DMABufferLock; Deltacast uses a single OPTIMAL image
imported as a CUDA array (copy buffer<->array) backed by RdmaGpuBuffer. AJA's
own comment notes OPTIMAL-as-array "scrambles per-tile on this driver", so a
shared transfer base is a redesign, not a mechanical move, and would need
Vulkan-RDMA hardware validation the GL smoke test doesn't provide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Rename the misleading "GpuDirect" generics (the interfaces are implemented by
every tier, including CPU staging — not GPU-direct-specific):

  GpuDirectStrategy            -> VideoOutputStrategy
  GpuDirectStrategyConfig      -> VideoOutputStrategyConfig
  GpuDirectCaptureStrategy     -> VideoCaptureStrategy
  GpuDirectCaptureStrategyConfig -> VideoCaptureStrategyConfig
  GpuDirectCaptureSlotRing     -> VideoCaptureSlotRing
  selectGpuDirectStrategy      -> selectVideoOutputStrategy
  GpuDirectOutput              -> RdmaVideoOutput
  HostStagedOutput             -> CpuStagedVideoOutput
  GpuRingBuffer                -> ImportedGpuBufferRing (a ring of QRhi buffers
                                  imported into CUDA, vs RdmaGpuBuffer's native
                                  RDMA VRAM)

Files renamed to match (file name == class name). Plugin-internal consumers
repointed. The old names live on as transitional `using` aliases (in the new
headers + the two forward-declaring nodes) plus stub old-path headers, so the
out-of-tree addon still builds unchanged until it repoints (Phase 3). Aliases +
stubs are removed in Phase 5. No behaviour change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…ake cleanup

The addon (its master) has repointed to the new names (Phases 3-4), so remove
the Phase-2 scaffolding: the `using` aliases in the renamed headers, the
forward-decl aliases in DMACaptureInputNode, and the six stub old-path headers
(GpuDirectStrategy/GpuDirectCaptureStrategy/GpuDirectStrategySelect/
GpuDirectOutput/HostStagedOutput/GpuRingBuffer). Plugin CMake source list
cleaned. The interop layer now exposes only the new, accurate names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…ept)

"P2P" was a misnomer: this CUDA driver-API layer also does VMM alloc,
GL/Vulkan/D3D external-memory import, semaphores, DtoD copies and dma-buf
export. Rename the file, the exported C ABI (cuda_p2p_* -> cuda_interop_*),
the handle/enum/error types (CudaP2P* -> CudaInterop*, CUDA_P2P_* ->
CUDA_INTEROP_*) and the export macro. All score-plugin-gfx call sites and
CMake updated. A transitional CudaP2PBridge.h shim aliases every old name to
the new one so the not-yet-repointed AJA addon keeps building; it is removed
in a follow-up once the addon is repointed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
The AJA addon is now repointed to <Gfx/Graph/interop/CudaInterop.h> and the
cuda_interop_* names, so the backward-compat alias header is no longer needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…ng clocks (Phase 1)

Introduce a RenderClock abstraction that owns HOW render ticks are delivered
to an output and guarantees the render callback runs on the render thread.
This is Phase 1 of the render-clock / genlock refactor: a pure,
zero-behaviour-change extraction that wraps the two clocks score already had.

- RenderClock (abstract): start(tick)/stop(), kind().
- TimerClock wraps clock #2 (the default): one shared HighResolutionTimer at
  manualRenderingRate + the coalesced set of outputs on it, mirroring the old
  GfxContext m_manualTimers entry. The per-output fan-out closure is the old
  on_manual_timer body; the timer is acquired from the same per-context
  score::Timers pool with the same Qt::QueuedConnection wiring.
- DisplayVSyncClock wraps clock #1: forwards the tick into
  OutputNode::setVSyncCallback (the swap-chain vsync push path) and clears it
  on stop().

GfxContext now holds vector<TimerClock> + a DisplayVSyncClock instead of the
inline timer->set<OutputNode*> map and the raw setVSyncCallback call. The
no_vsync graph-update pump and the watchdog timer are unchanged (they are not
render clocks). Coalescing, rates, timer lifetimes and vsync semantics are all
byte-identical.

Validated behaviour unchanged:
- PipewireRoundtrip --only s2s: all 14 cells PASS.
- AJARoundtrip 1080p5994 YCbCr8 rdma rx-gpu (Kona5-8K, conn pair 3): PASS
  52.26 dB, RDMA-GL/T3 engaged, out-ring drops still present (timer path).

Phase 2 (opt-in AjaGenlockClock : ExternalGenlockClock driven by the card VBI)
is not part of this commit. RFC: ~/ossia/rdma-test-utils/render-clock-rfc.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Add the hardware-genlock render clock (the pull facet of the RenderClock
model from Phase 1): a dedicated tick thread blocks on an injected
waitForNextTick() (a card's output VBI) and marshals render() onto the
render thread via Qt::BlockingQueuedConnection — blocking so the next VBI
wait begins only after the frame is rendered+pushed, keeping render
phase-locked to the card and never bursting ahead. The pull facet is
injected as a std::function, so the gfx layer stays vendor-SDK-free.

DirectVideoOutputBackend gains an optional genlockTickSource() (default
empty = no hardware genlock; the node keeps its timer clock);
DirectVideoOutputNode forwards it. Nothing here is engaged unless a caller
builds an ExternalGenlockClock — the default paths are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
@jcelerier jcelerier force-pushed the split/gpu-interop branch from 3fb5a92 to 17ef50a Compare July 12, 2026 02:00
~Minimal[GUI]Application ran QApplication::processEvents() AFTER
delete m_presenter, dispatching deferred slots queued during plugin load
(Scenario::SearchWidget deferred-init → findDeviceExplorerWidgetInstance)
that read the presenter-owned application context — a heap-use-after-free,
and the SIGSEGV the non-sanitized harnesses hit on shutdown. Drain the
event queue BEFORE deleting the presenter; remaining deferred deletes still
flush via ~QApplication. Found by ASan on the DeckLink/Magewell harnesses;
--list now exits cleanly (was SIGSEGV).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
@jcelerier

Copy link
Copy Markdown
Member Author

superseded by #2109

@jcelerier jcelerier closed this Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants