Status: in progress (approved 2026-09-22). This document is the contract the work packages below are built against; change it before changing an interface.
Measured on the reference machine (RTX 5090, 32 cores, sample clip
example_footage_dlogm.OSV, dual 3000x3000 10-bit HEVC at 59.94 fps):
| Stage, one 2560x1440 view | Old Premiere pipeline | Hardware floor |
|---|---|---|
| Decode both lenses | 5-50 ms + a host copy (software: 949 ms median to park on a new frame) | 2.1 ms - NVDEC sustains 467 frame pairs/s, D3D11VA 496, frames stay in VRAM |
| Build the view | stitch a 6000x3000 equirect on the GPU, read 18 MP back, convert it on the CPU, Premiere uploads it again, then reframe it (CPU path: 12 ms) | ~0.3 ms - one kernel from the fisheyes into Premiere's GPU frame |
| Parallax measurement | 16 ms CPU DIS per 8-frame bucket (was 205 ms) | ~1-2 ms on the GPU |
The old design routes a full stitched sphere through host memory so that a 2560x1440 window can be cut out of it. That round trip, not any arithmetic, is the cost. It is also a quality loss: two resamplings (fisheye -> equirect -> view) instead of one, and a 90 degree view at 2560 wide needs a 10240-wide equirect for 1:1 sampling - the fisheyes have that density, the 6000-wide equirect does not.
Premiere render thread (GPU filter Render)
|
Open360Reframe.aex --- C ABI ---> OpenOSVImporter.prm (the ENGINE lives here)
thin client: ClipEngine registry (per source file):
- finds its source clip - metadata, rig, colour, stabilisation
(Video Segment node walk) - GpuClipDecoder: NVDEC into Premiere's
- maps clip time -> media frame CUcontext, VRAM frame cache, GOP-aware,
- passes view params, CUcontext, decode-ahead (WP-A)
stream, output device pointer - GPU analyses: band shading + CUDA DIS
-> seam table / gain / parallax (WP-B)
- fused kernel: fisheyes -> view,
straight into Premiere's frame (WP-C)
- One process-wide engine, inside the importer module, exported through a
versioned C ABI (
OsvEngine_*). The effect never links the engine; it finds the loaded importer module and resolves the entry points, so the two plug-ins keep independent builds and a missing/old importer degrades cleanly to the existing equirect path. - Everything per frame stays on the GPU in Premiere's own CUDA context. The only host traffic is parameters and a few KB of analysis results.
- The CPU path (software renderer, Media Encoder without a GPU) keeps working through the existing equirect route until the same kernel is wired to CPU decode; output must match the GPU path.
- The importer keeps producing the equirect (for users who want the 360 clip itself). It is NOT degraded when the effect is present - the request carries no consumer and the host's cache key none either, so a cheaper frame would reach the Source Monitor, other sequences and the effect's own fallback; instead the importer's own frame runs on the GPU too (NVDEC into VRAM, stitch in place, pinned banded readback: ~6x cheaper while playing). See docs/PREMIERE.md, "The importer's own frame".
New files: include/osv/video/GpuClipDecoder.h, src/osv/video/GpuClipDecoder.cpp,
tests tests/unit/test_gpu_decoder.cpp.
-
NVDEC through FFmpeg's CUDA hwaccel, decoding into an externally supplied
CUcontext(av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA), setAVCUDADeviceContext::cuda_ctx(andstreamwhen given),av_hwdevice_ctx_init). With no context supplied, use the device's primary context (AV_CUDA_USE_PRIMARY_CONTEXT). Never create a private context per decoder. -
CUDA driver API only in new code (
cuda.h): the Premiere side must not depend on a cudart DLL. -
Own VRAM frame store: each decoded NVDEC surface pair is copied (
cuMemcpy2DAsync, device to device) into pooled buffers, so FFmpeg's small surface pool is never pinned by the cache. Layout per lens: P010, luma plane plus interleaved CbCr, one pitch - exactly whatDeviceFrameRefandfillDevicePlane()describe. -
Cache policy: keyed by frame index; capacity from a VRAM budget (default: min(1.5 GB, 20 % of free VRAM), overridable). A random access to frame k decodes from the previous sync sample and keeps every frame it decoded on the way, so stepping backwards/forwards inside that GOP is a cache hit. Eviction: least recently used, never a leased slot.
-
Decode-ahead: a worker thread that, after sequential access is detected, decodes up to N frames ahead (default 8) into the cache; cancellable; pushes/pops the context itself.
-
API (settled;
include/osv/video/GpuClipDecoder.his the reference):struct GpuDecoderOptions { void* cuContext = nullptr; // CUcontext; nullptr = primary context of cudaDevice (retained) void* cuStream = nullptr; // CUstream for NVDEC post-processing and every copy; // nullptr = one owned non-blocking stream per lens int cudaDevice = 0; std::size_t vramBudgetBytes = 0; // 0 = min(1.5 GiB, 20 % of free VRAM) std::uint32_t decodeAhead = 8; // 0 = no worker thread int decoderThreads = 3; // libavcodec frame threads per lens decoder }; enum class LeaseSource { CacheHit, WaitedForDecode, Decoded }; class GpuFrameLease { // move-only; pins one cached pair public: bool valid() const; std::uint32_t frameIndex() const; const FramePair& pair() const; // device[] filled, lens[] sized, no host planes; // every owner field shares the pin LeaseSource source() const; Status releaseAfter(void* cuStream); // event on the caller's stream; the slot's // next overwrite waits for it on the GPU void release(); // caller asserts no pending GPU reads (= destructor) }; class GpuClipDecoder { public: static Result<std::unique_ptr<GpuClipDecoder>> open(const std::filesystem::path&, const meta::FormatInfo&, const GpuDecoderOptions& = {}); static bool available(std::string* reason = nullptr); Result<GpuFrameLease> acquire(std::uint32_t frameIndex); // thread-safe bool isCached(std::uint32_t frameIndex) const; std::uint32_t dropCachedFrames(); GpuDecoderStats stats() const; // frameCount(), fps(), lensWidth(), lensHeight(), cuContext(), deviceIndex() };
-
Decisions made while building it:
- No context supplied: the primary context is taken with
cuDevicePrimaryCtxRetainand handed to FFmpeg as an external context. FFmpeg's ownAV_CUDA_USE_PRIMARY_CONTEXTpath insists onCU_CTX_SCHED_BLOCKING_SYNCand fails ("Primary context already active with incompatible flags") as soon as cudart or a renderer activated the primary context with the default flags; retaining it leaves the flags alone. - FFmpeg's CUDA device context always gets a stream - the caller's, or a non-blocking stream per lens owned by the decoder. With FFmpeg's default (the legacy NULL stream) its surface copies would serialise against every blocking stream of a shared context, i.e. against Premiere's rendering. NVDEC post-processing, FFmpeg's copy and ours are then ordered on one stream per lens without any host wait.
HevcStreamDecodergainedDecoderOptions::cudaContext/cudaStreamandpreviousSyncIndex();GpuClipDecoderdrives two of them (container sample feed,keepOnDevice) instead of growing a second FFmpeg front end. With no context suppliedHevcStreamDecoderbehaves exactly as before.acquire()returns complete data: the first acquire of a freshly filled slot waits on the host for that slot's two copy events (the two device-to-device copies of one frame pair, recorded right behind the decode), so consumers need no ready-event protocol and may read on any stream of the context.- A
FramePaircopied out of a lease keeps the slot pinned (its owner fields share the pin), so aRenderJobholding the pair is safe on its own; releasing the lease alone does not unpin such a copy. open()decodes frame 0 once more after the lens decoders' probe and keeps it: frame 0 is cached and the decoder sits on frame 1, so the first landing in the first GOP continues instead of paying a flush and a re-decode of frame 0 (~6 ms of every cold park there).- The decode-ahead window is clamped to
capacity - 2slots, and the worker never evicts a frame inside its own window (it would chase its own tail); the foreground may evict anything that is not pinned. A request more than 4 frames away from the running window cancels it. - NVDEC's own decode surfaces are outside the cache budget: 790 MiB for both 3000 x 3000 lenses at 3 frame threads right after open, ~880 MiB once FFmpeg's output pool has grown during playback (each frame thread adds one ~26 MiB surface per lens). One thread saves 156 MiB but misses the cold-park target (66 ms median); 3 threads is the knee.
nvcuda.dll: the importer will import it directly once it linksGpuClipDecoder(WP-D). Add it toOSV_IMPORTER_DELAYLOAD_DLLSthen, as the effect already does, so a machine without an NVIDIA driver still loads the importer.
- No context supplied: the primary context is taken with
-
Measured (RTX 5090, driver 616.56, sample clip,
osv_gpu_decode_bench, three runs): open 50 ms; park median 46-48 ms cold-GOP (a fresh decoder per landing) and 40-42 ms in-session (one decoder, cache dropped before each landing, decoder position carried over); 0.001 ms on a cache hit; sequential with decode-ahead 514-524 frame pairs/s (520-532/s when the host itself drives the decoder flat out); cache 1529 MiB of a 1536 MiB budget (29 slots of 52.7 MiB); destruction mid decode-ahead 43-47 ms with no VRAM left behind (unit test). The GOP catch-up runs at ~1.9 ms per pair (landing on 45 from frame 1: 84.5 ms), a little under the 2.1 ms per pair of the ffmpeg measurement above. -
Thread safety: several Premiere render threads call
acquireconcurrently. -
Acceptance: frames bit-exact with software decode (after the P010 shift) at scattered indices; park (12 scattered frames) median < 60 ms cold-GOP and < 1 ms on a cache hit; sequential with decode-ahead >= 300 frame pairs/s on the reference machine; VRAM stays under budget; destruction while the worker is decoding neither hangs nor leaks.
New files under src/osv/render/cuda/ plus tests tests/unit/test_disflow_cuda.cpp.
- A CUDA DIS solver with the same maths and DJI constants as
src/osv/render/DisFlow.cpp(Kroeger et al., ECCV 2016: pyramid, inverse structure tensors, inverse-search patch solve, photometric-weight densify, Gaussian smoothing, forward/backward consistency). Exposed as a newFlowBackendKindsocomputeFlow()/parallaxFromBands()use it unchanged. Parity with the CPU solver is tolerance-based (float order differs): mean endpoint difference < 0.05 px on the sample bands, and the resulting parallax grid's gated cells within 2 %. - Band shading on the GPU:
renderLensBands()must work when the frames are device-resident (RenderJob::planesOnDevice), shading only the band rows on the GPU and downloading the small luma/alpha bands. This makes seam search, gain estimation and the parallax measurement all work fromGpuClipDecoderframes. The library dependency runs cpu -> cuda, so the CPU library needs an injectable band shader (or an equivalent seam) rather than a direct call. - Acceptance: whole parallax measurement for one bucket (bands + bidirectional flow + grid) < 3 ms on the reference machine from device frames.
Settled (WP-B delivered). Interface, as built:
// include/osv/render/CudaAnalysis.h (osv_render_cuda)
bool cudaAnalysesAvailable(std::string* reason = nullptr);
Status installCudaAnalyses(); // installs both hooks below; Unsupported without a GPU
void uninstallCudaAnalyses() noexcept;
bool cudaAnalysesInstalled() noexcept;
Result<BidirFlow> cudaDisFlowBidirectional(const GrayImage&, const GrayImage&,
const DisFlowParams&, void* stream = nullptr);
Result<BidirFlow> cudaDisFlowBidirectionalDevice(const float* a, const float* b, uint32_t w,
uint32_t h, size_t pitchBytes,
const DisFlowParams&, void* stream = nullptr);
Result<std::vector<float>> cudaShadeBandRgba(const RenderJob&, uint32_t row0, uint32_t row1,
void* stream = nullptr);
Status cudaShadeBandLumaAlpha(const RenderJob&, uint32_t row0, uint32_t row1,
std::vector<float>& luma, std::vector<float>& alpha,
void* stream = nullptr);
// osv_render_cpu seams the CUDA library installs into
enum class FlowBackendKind { Auto = 0, Classical = 1, Neural = 2, ClassicalCuda = 3, Count };
void setCudaFlowBackendFactory(FlowBackendFactory); // FlowBackend.h
class DeviceBandShader; // DeviceBandShader.h
void setDeviceBandShader(std::shared_ptr<DeviceBandShader>);
Result<ParallaxWarpGrid> gridFromFlow(bands, flow, params, ThreadPool* pool = nullptr);A caller with device-only frames calls installCudaAnalyses() once; after
that renderLensBands / searchSeam / estimateGain / overlapNcc /
measureParallaxBands shade the band rows on the GPU automatically whenever
the job's planes are on the device, and ParallaxWarpParams::backend = FlowBackendKind::ClassicalCuda puts the flow there too. Host frames keep
the CPU band path unchanged. Everything runs in the CUDA context current on
the calling thread (never cudaSetDevice / cudaDeviceReset /
cudaDeviceSynchronize), ordered on the caller's stream or a pooled
per-context one, synchronising only that stream.
Parity: the CUDA DIS field is bit-identical to DisFlow.cpp (synthetic
pairs and the sample's real bands, frames 0/32/64, compared with ==), so
the parallax grids are identical too; GPU band shading is 108-111 dB PSNR
against the CPU band path. Measured (osv_gpu_analysis_bench, medians,
frames 0/32/64): bands 0.25 ms, bidirectional flow 0.79 ms, grid 0.95-1.2 ms,
end to end 2.1-2.7 ms; the CPU path on 32 threads is 21-23 ms.
Deviations: the grid (gridFromFlow) stays on the CPU - it was ~7-9 ms
single-threaded, which nobody had measured, and is now split over the pool
and restructured to ~1 ms with a byte-identical result. Auto never picks
ClassicalCuda (existing selections do not change underneath callers); the
plug-in preference enum is not extended yet (WP-D). The analyses use the
CUDA runtime (cudart_static, as osv_render_cuda already does), bound to
whatever driver context is current - not the driver-only API the plug-in
rule asks of new Premiere-side code.
Files under plugins/reframe/ (kernel added to ReframeKernel.cu's fatbin or a
sibling), tests under tests/premiere/reframe/.
-
A driver-API kernel that runs
osvShadePixelW()inOSV_MODE_REFRAMEfrom device P010 planes (+ seam table, + warp grid) and writes Premiere GPU frame pixels (BGRA 32f or 16f, top-left origin, pitch in bytes). -
A host-side parameter builder that turns the effect's
Settings(pan, tilt, roll, FOV, distortion, preset, source pan/tilt/roll, output resolution cover-fit, sequence size) plus the clip's stitch state (rig, colour, blend, seam/warp, per-frame stabilisation rotation) intoOsvRenderParams. The framing must be identical to the existing two-step path (importer equirect ->osvReframeEquirectPixel): same rotation conventions, same eye-offset/distortion mapping, same cover-fit. IfOsvRenderParamslacks a field the equirect path relies on (e.g. a viewport rectangle), add it toosv_kernel.hin all dialects and keep the parameter block <= 4 KB. -
Acceptance: against the two-step path on the sample clip at several views (rectilinear, wide eye-offset, tiny planet, across the seam, rolled), the direct render has the same framing (geometric alignment within 0.5 px, measured) and is at least as sharp (higher high-frequency energy); a CPU-reference twin of the kernel agrees with the GPU to PSNR >= 60 dB.
-
Settled interface (done;
plugins/reframe/DirectRender.h,DirectLaunch.h,DirectKernelAbi.h):struct StitchState { // one source frame OsvRenderParams equirect; // the importer's equirect block for the frame: exactly // RenderParamsBuilder.rig.color.blend.alphaCoverage.gain // .seam|.warp.stabilization(stabilizationFor(i)) // .equirect(Standard).buildParams() const float* seamTable; // device ptr (GPU) / host ptr (CPU twin), or null const float* warpGrid; // same rule }; DirectSetup buildDirectParams(const Settings&, const StitchState&, int outW, int outH, SizePx sequenceSize) noexcept; // valid / DirectReject DirectLaunchResult launchDirect(CUfunction kernel /* kDirectKernelName */, CUstream stream, const DirectSetup&, const OsvPlane devicePlanes[2], const DirectOutput& out /* data, rowBytes, w, h, isHalf */) noexcept; bool renderDirectCpu(const DirectSetup&, const OsvPlane hostPlanes[2], const FrameView& dst, ThreadPool*) noexcept; // CPU twin / fallback
The kernel
osvReframeDirectKernellives in the effect's existing fatbin (cuModuleGetFunction(module, kDirectKernelName)on the module GpuFilter already loads); planes come fromrender::fillDevicePlane()of the P010 frames. The camera isbuildView()- the functionbuildParams()uses - andRout = R_stab * Rout_view, whereR_stabis the equirect block's own Rout (an equirect block has no camera). Noosv_kernel.hchange was needed: the viewport is always the whole frame and the cover-fit is folded intofocalPx;buildDirectParamsrefuses a sub-rectangle viewport so that assumption cannot fail silently.launchDirectchecks every plane, table and output address withcuPointerGetAttribute(device memory, this device, inside its allocation) before launching; WP-D should log aDirectLaunchRejectand fall back to the equirect path. -
Measured (RTX 5090, sample clip,
tests/premiere/reframe/test_direct.cpp): framing error <= 0.0016 output px over 8 views x 2 stabilisations (negative control 453 px); sample-clip NCC 0.9995-0.9999 with the peak at zero offset (sub-pixel <= 0.044 px); mean gradient 1.04-1.12x the two-step render (1.12x at 40 deg); GPU vs CPU twin 95-112 dB (32f and 16f, seam table and warp grid, NVDEC zero-copy frames); kernel 0.26-0.29 ms at 2560x1440, 0.65-0.76 ms at 3840x2160.
OsvEngine_*C ABI exported from the importer; ClipEngine registry keyed by file identity; prefs published by live importer instances.- Effect: node walk to the media node (evidence from the
reframe/gpu/source:probe log), clip time -> media frame viaTransformNodeTime, output colour in the sequence working space, fallback to the equirect path on any failure. - Prerequisite, open: Premiere must call our GPU filter at all. Every recorded
session loaded
xGPUFilterEntryand never calledCreateInstance; the out-flags now match the two known-good GPU effects (db6eb14) pending a test.
Field report (Premiere 26.2.2): Exposure -1 in a clip's Source Settings darkened the Source monitor (the importer's equirect) but not the Program monitor (the effect's direct render); colour and calibration changes "did not apply".
What the field log proves. Session pid 63724: after every Source Settings
change Premiere re-opened the clip (imOpenFile8 on a NEW importer instance,
imGetInfo8 with the new blob), created 2-3 new GPU filter instances within
~25 ms (23:46:51.947 publish, .970 CreateInstance x2) and asked the new
importer instance for the playhead frame at full size. So Premiere rebuilt
and re-rendered the clip's nodes; what the direct path rendered WITH is where
the gaps were. The old instances stay alive meanwhile.
Why Premiere re-renders (the SDK mechanism relied on). The direct path depends on exactly what the effect's input frame depends on - the media file, the clip's importer prefs, the time - so Premiere's own invalidation of the input is a sufficient invalidation of the direct output:
- SDK guide 11.8.2 "Frame Caching": "If the user has modified the filter settings, the clip settings, the preview quality, etc, Premiere will call the filter to render with the new settings".
- PPix Cache Suite v2+: "frames are differentiated within the cache, based on the importer preferences, so when the preferences change, the host will not use the old frame".
- Importer guide,
imGetPrefs8: "If the user changes the Clip Source Settings in a way such that the frames should be reimported, then the importer should use the Importer File Manager Suite to call RefreshFileAsync()"; "each clip has its own prefs, and the host application automatically passes the correct prefs to the appropriate importer instance". PrSDKGPUFilter.h: "A filter instance represents an effect applied to a track item with a fixed set of parameters. Changed parameters will create a new instance" - and the log shows new instances after Source Settings changes too (the clip's segment was rebuilt).
No SDK hook lets a GPU filter force a re-render or mix its own key into the
render cache (the AE GuidMixInPtr belongs to SmartFX PreRender, which the
GPU filter path does not run; PF_OutFlag_NON_PARAM_VARY declares output
that changes with NO tracked input and would give up frame caching for every
clip the effect is on). So the design makes the direct output a pure
function of what Premiere already tracks, and PROVES the re-render in the
log:
- File identity. The engine keys clips and settings by volume serial +
file index (
GetFileInformationByHandle), not path spelling; tested with case,/,\\?\,./..and a hard link. Each spelling is logged once with its identity. - Newest instance wins. Every publication carries the publishing
instance's age (a token taken at its first publication, right after
imOpenFile8). An older instance can never undo a newer one's settings; an instance the host gave no blob publishes its defaults only into a blank. A host blob equal to the defaults is still published (the "exposure back to 0" case). - Generation. Each change bumps a per-file generation, reported with every
frame (
OsvEngineFrame::settings) and byOsvEngine_QuerySettings(no decode, no GPU), which the effect asks before decoding. - Never render unknown settings. Generation 0 (nothing published for the file in this process) hands the frame to the equirect route: a publication failure can no longer show silently wrong settings. Premiere always opens and describes a clip (publishing it) before it can hand the effect a frame of it, so this costs nothing in practice - and it keeps proxy-only sessions on the proxy.
Colour semantics. On the equirect route the importer encodes its frame in
the clip's colour output and Premiere converts that into the working space -
what the Source monitor shows. The direct path renders straight from the
fisheyes into the working space, so Premiere's conversion never runs. The
clip's Source Setting Program Monitor Colour (PrefsBlob::directColour,
offset 24; Source Settings effect, Advanced group) decides what happens when
the colour output is not the working space:
| Clip colour output \ working space | PQ | HLG | Rec.709 |
|---|---|---|---|
| PQ | direct, identical | direct, differs (S) / equirect (M) | direct, differs (S) / equirect (M) |
| HLG | direct, differs (S) / equirect (M) | direct, identical | direct, differs (S) / equirect (M) |
| Rec.709 | direct, differs (S) / equirect (M) | direct, differs (S) / equirect (M) | direct, identical |
| D-Log M passthrough | equirect | equirect | equirect |
(S) = Sequence space (fast), the default - value 0, so new clips AND
every older project's zero byte get it. (M) = Match Source monitor, the
per-clip opt-in. OSV_DIRECT_COLOR=working|match overrides the per-clip
choice process-wide for A/B sessions. Every route line in the log names the
rule that chose it (settings unknown, working space, same space,
D-Log M passthrough, Sequence space (fast), Match Source monitor).
- Identical (the diagonal, rule "same space"): the engine's colour block is
byte-identical to the importer's (
makeColorParamswith the same fit, transfer, exposure, input, range and bit depth - pinned for PQ, HLG, Rec.709 and the passthrough at exposure -1 - and the same Rec.709 look and PQ HDR peak, see docs/COLOR.md) and Premiere's source -> working conversion is the identity, so the Program monitor matches the Source monitor route up to that route's second resampling (WP-C: NCC 0.9995-0.9999, framing <= 0.0016 px); the direct render is the sharper one. - Differs, by design (off the diagonal, default): PQ, HLG and Rec.709 are
three encodings of one scene, and the direct path renders that scene
straight into the working space with OpenOSV's own tone mapping - the
colour-managed answer, just not Premiere's generic conversion. The Source
monitor shows Premiere's conversion of the importer's frame: HDR -> SDR
tone mapping with the clip's gamut-map controls (
ClipNode::GamutMapControlsin the field log), SDR placed at graphics white (203 nits) in an HDR container, HLG <-> PQ at its nominal peak. The SDK exposes none of that (the Colour Management Suite only describes spaces), so the two monitors differ by however far Premiere's tone mapper is from ours. In the user's everyday setup - PQ clips (the default output) in Rec.709 sequences (every installed preset's working space is "BT.709 RGB Full") - the Program monitor therefore shows OpenOSV's Rec.709 rendering, the DJI Studio look WP-LOOK is matching, while the Source monitor shows Premiere's PQ -> 709 tone map. Choosing Rec.709 as the colour output makes both monitors agree; "Match Source monitor" makes the Program monitor follow Premiere instead, at the cost of the direct path's speed and sharpness. - D-Log M passthrough: the importer declares the log signal as "BT.2020
RGB Full (Scene)" (SEI build: primaries 9, transfer 2 "unspecified";
PrefsMapping.cpp), deliberately never 709 (that would present flat log as a finished picture). None of the working spaces the direct path produces (PQ 9/16, HLG 9/18, Rec.709 1/1-6-14-15;transferForSeiCodes) is that space, so "same space" can never apply and the clip always takes the equirect route: its look is Premiere's interpretation, and grading it would make a downstream LUT double-convert. Were a working space ever equal to the passthrough's own encoding, the generic "same space" rule would render it exactly - the engine test pins that the passthrough colour block is then byte-identical to the importer's.
ABI (not bumped; the lead bumps once at merge). OsvEngineClipSettings
(structSize, generation, clipTransfer, exposureStops, colorOutput,
calibration, dlogmFit, stabilization, directColour, fileVolume, fileIndex);
OsvEngineFrame::settings appended; optional export
OsvEngine_QuerySettings(path, settings*, error, capacity).
What the next Premiere session logs (importer log, then effect log):
direct: 'L:\...\clip.OSV' is file ec41ff64:00080000000d7852 (once per spelling)
direct: Source Settings generation 2 for 'clip.OSV' (file ...) from importer
instance #5 (importer id 14434?): colour 0, fit 2, exposure -1.00, ...
direct: kept Source Settings generation 2 ... instance #3 is older and holds ... (if it happens)
direct: engine now renders 'clip.OSV' with Source Settings generation 2: ... (was ...)
reframe/direct: 'clip.OSV' media node hash {GUID}, mod state ..., clip id 14434
reframe/direct: 'clip.OSV' media node CHANGED - hash ... (was ...) (if Premiere's identity moves)
reframe/direct: 'clip.OSV' Source Settings generation 2 (colour PQ, exposure -1.00 stops, ...,
Program Monitor Colour Sequence space (fast); file ...) in a Rec.709 sequence
-> straight from the fisheyes (rule: Sequence space (fast)): rendering the scene straight
into the Rec.709 working space with OpenOSV's tone mapping; the Source monitor shows
Premiere's own PQ -> Rec.709 conversion, so the two differ
(The last line is verbatim from the settings-change end-to-end test, apart
from the working space; the other rules read (rule: same space),
(rule: Match Source monitor), (rule: D-Log M passthrough),
(rule: settings unknown) and (rule: working space).)
The effect's generation line appears on the FIRST render after each change, so it is the proof that Premiere re-rendered; a publish line without it would mean Premiere served a cached output. The importer id in the publish line and the media node's ClipID are logged side by side to find out whether they are the same number - if so, settings could be bound per clip rather than per file (today two master clips of one file with different settings share the newest instance's settings).
Known limitations and hand-offs. Two master clips of the same file with
different Source Settings share one set on the direct path (the newest
instance's). Handed to WP-CALIB: the modal dialog (SourceSettingsDialog.cpp
/ PrefsMapping.cpp) calls RefreshFileAsync only when it has an instance
(imGetInstancePrefs), not from imGetPrefs8; it has no Program Monitor
Colour combo yet, and because prefsFromControls rebuilds the blob from
PrefsBlob::defaults(), clicking OK resets every field it shows no control
for (Program Monitor Colour - now onto its default, so harmless unless the
user chose Match Source monitor - and already parallax / flow backend).
Handed to WP-IMPORTER: the connection-space override in imGetSourceVideo
(host selects "BT.709 RGB Full") goes through applyPrefsLocked and is
therefore published like a user change. Effects placed BEFORE Open 360
Reframe on the clip are bypassed by the direct path (it never reads its
input frame).
- Heavy Doxygen, defensive checks, no TODOs or stubs, no fake data.
- CUDA driver API in anything the plug-ins load;
-fmad=false, no fast-math. - Tests never touch the user's real plug-in logs (
isolatePluginLogs()in any new Premiere test main);[sample]tests readOSV_SAMPLE_FILE. - Never launch Premiere; never install plug-ins.