From 1a491657f15156c7aa245130f52e1429f0b10656 Mon Sep 17 00:00:00 2001 From: Gary Oberbrunner Date: Wed, 23 Sep 2026 21:06:16 -0400 Subject: [PATCH 1/4] Support: fix the Noise example rendering differently in tiles The Noise generator seeded a Mersenne Twister in every call to multiThreadProcessImages, from the time and the first row of that call's window, then drew values in scan order. A pixel's noise therefore depended on where its tile and its thread's slice of the tile began, so a frame rendered in tiles, or on a machine with a different number of threads, differed from the same frame rendered whole. Each pixel's noise is now a hash of the time-based seed, its position and the component, so it depends only on those and the noise level. The distribution is unchanged: uniform from 0 to the noise level. Assisted-by: Claude Code / Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Gary Oberbrunner --- Support/Plugins/Generator/noise.cpp | 28 +++++++++++++++++++--------- release-notes-next.md | 1 + 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Support/Plugins/Generator/noise.cpp b/Support/Plugins/Generator/noise.cpp index 249d1839..e4592395 100644 --- a/Support/Plugins/Generator/noise.cpp +++ b/Support/Plugins/Generator/noise.cpp @@ -8,7 +8,16 @@ #include "../include/ofxsProcessing.H" -#include +#include + +/** @brief mix the bits of v thoroughly (the splitmix64 finaliser) */ +static inline uint64_t hash64(uint64_t v) +{ + v += 0x9e3779b97f4a7c15ULL; + v = (v ^ (v >> 30)) * 0xbf58476d1ce4e5b9ULL; + v = (v ^ (v >> 27)) * 0x94d049bb133111ebULL; + return v ^ (v >> 31); +} //////////////////////////////////////////////////////////////////////////////// // base class for the noise @@ -46,24 +55,25 @@ public : // and do some processing void multiThreadProcessImages(OfxRectI procWindow) { - float noiseLevel = _noiseLevel; - - // set up a random number generator // Distribution is from 0 to pixel max level times noise level - std::random_device rd; - std::mt19937_64 mt(rd()); - mt.seed(_seed + procWindow.y1); - std::uniform_real_distribution dist(0.0, max * noiseLevel); + double scale = max * _noiseLevel; + + // Each pixel's noise is a hash of the seed and its position, so a frame + // comes out the same however it is split into tiles and threads + uint64_t seedHash = hash64(_seed); // push pixels for(int y = procWindow.y1; y < procWindow.y2; y++) { if(_effect.abort()) break; PIX *dstPix = (PIX *) _dstImg->getPixelAddress(procWindow.x1, y); + uint64_t rowHash = hash64(seedHash ^ uint32_t(y)); for(int x = procWindow.x1; x < procWindow.x2; x++) { + uint64_t pixelHash = hash64(rowHash ^ uint32_t(x)); for(int c = 0; c < nComponents; c++) { - double randValue = dist(mt); + // the top 32 bits of the hash, as a uniform random number in [0, 1) + double randValue = scale * (double(hash64(pixelHash + c) >> 32) / 4294967296.0); if(max == 1) // implies floating point, so don't clamp dstPix[c] = PIX(randValue); diff --git a/release-notes-next.md b/release-notes-next.md index 4b8ceac3..85883571 100644 --- a/release-notes-next.md +++ b/release-notes-next.md @@ -27,6 +27,7 @@ This is version NEXT of the OpenFX API. - Fixed the ColourSpace example's `OfxSetHost` to have the proper signature so it actually gets called. - Fixed the `@propdef` metadata of `kOfxParamPropChoiceEnum` (a string array, not a bool) and `kOfxParamPropDimensionLabel` (one label per dimension, not one). - Fixed the Invert example never releasing its output image (a shadowed handle variable). +- Fixed the Support Noise example giving a different image when a frame is rendered in tiles: it seeded its noise per render call, and now each pixel's noise depends only on its position, the time and the noise level. ## Deprecations From 97200380038cf8c65aa1bce54a228ec4fee38be4 Mon Sep 17 00:00:00 2001 From: Gary Oberbrunner Date: Wed, 23 Sep 2026 21:06:44 -0400 Subject: [PATCH 2/4] Support: stop the GPUGain example offering an Alpha output it can't render GPUGain's output clip listed Alpha as well as RGBA among its supported components, but its source clip takes only RGBA and every render path, CPU, CUDA, Metal and OpenCL, processes four floats a pixel. A host that chose Alpha for the output got kOfxStatErrUnsupported from the render, and would otherwise have hit the check that the source and output components match. The output clip now supports only RGBA, which is what the plugin renders. Assisted-by: Claude Code / Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Gary Oberbrunner --- Support/Plugins/GPUGain/GPUGain.cpp | 1 - release-notes-next.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Support/Plugins/GPUGain/GPUGain.cpp b/Support/Plugins/GPUGain/GPUGain.cpp index 10a6d057..0c2832fe 100644 --- a/Support/Plugins/GPUGain/GPUGain.cpp +++ b/Support/Plugins/GPUGain/GPUGain.cpp @@ -414,7 +414,6 @@ void GPUGainFactory::describeInContext(OFX::ImageEffectDescriptor& p_Desc, OFX:: // Create the mandated output clip ClipDescriptor* dstClip = p_Desc.defineClip(kOfxImageEffectOutputClipName); dstClip->addSupportedComponent(ePixelComponentRGBA); - dstClip->addSupportedComponent(ePixelComponentAlpha); dstClip->setSupportsTiles(kSupportsTiles); // Make some pages and to things in diff --git a/release-notes-next.md b/release-notes-next.md index 85883571..3fd86c7b 100644 --- a/release-notes-next.md +++ b/release-notes-next.md @@ -28,6 +28,7 @@ This is version NEXT of the OpenFX API. - Fixed the `@propdef` metadata of `kOfxParamPropChoiceEnum` (a string array, not a bool) and `kOfxParamPropDimensionLabel` (one label per dimension, not one). - Fixed the Invert example never releasing its output image (a shadowed handle variable). - Fixed the Support Noise example giving a different image when a frame is rendered in tiles: it seeded its noise per render call, and now each pixel's noise depends only on its position, the time and the noise level. +- Fixed the Support GPUGain example failing to render when the host chose Alpha for its output: it declared Alpha output but processes only RGBA, so its output clip now supports only RGBA. ## Deprecations From 29c555ceb67d068c05c6b8e62f06717e77fbc193 Mon Sep 17 00:00:00 2001 From: Gary Oberbrunner Date: Wed, 23 Sep 2026 21:07:50 -0400 Subject: [PATCH 3/4] Support: fix the Gamma example turning negative values into NaNs The Gamma example in the MultiBundle plugins raised each component to its gamma with pow, which is NaN for a negative value and a fractional exponent, and flips the sign for an even integer one. Float images may hold negative values, and the other Support and Guide examples let them through their float paths rather than clamping them, so the Gamma example produced NaNs wherever its input went below zero. It now applies the gamma to the magnitude and keeps the sign, which leaves non-negative values as they were and makes the curve symmetric about zero, as the gain examples are. Assisted-by: Claude Code / Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Gary Oberbrunner --- Support/Plugins/MultiBundle/multibundle1.cpp | 4 +++- release-notes-next.md | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Support/Plugins/MultiBundle/multibundle1.cpp b/Support/Plugins/MultiBundle/multibundle1.cpp index bc1e9444..dddf355c 100755 --- a/Support/Plugins/MultiBundle/multibundle1.cpp +++ b/Support/Plugins/MultiBundle/multibundle1.cpp @@ -120,7 +120,9 @@ public : { for(int c = 0; c < nComponents; c++) { - float v = (float)(pow((double)srcPix[c], (double)scales[c])) * maskScale + (1.0f - maskScale) * srcPix[c]; + // apply the gamma to the magnitude and keep the sign, since pow of a negative value is NaN for a fractional exponent + double s = srcPix[c]; + float v = (float)(copysign(pow(fabs(s), (double)scales[c]), s)) * maskScale + (1.0f - maskScale) * srcPix[c]; if(max == 1) dstPix[c] = PIX(v); else diff --git a/release-notes-next.md b/release-notes-next.md index 3fd86c7b..0d103bef 100644 --- a/release-notes-next.md +++ b/release-notes-next.md @@ -29,6 +29,7 @@ This is version NEXT of the OpenFX API. - Fixed the Invert example never releasing its output image (a shadowed handle variable). - Fixed the Support Noise example giving a different image when a frame is rendered in tiles: it seeded its noise per render call, and now each pixel's noise depends only on its position, the time and the noise level. - Fixed the Support GPUGain example failing to render when the host chose Alpha for its output: it declared Alpha output but processes only RGBA, so its output clip now supports only RGBA. +- Fixed the Support Gamma example producing NaNs from negative float input: it now applies the gamma to a value's magnitude and keeps its sign. ## Deprecations From db28d1a18c26874179a0cde1e261633c836681dd Mon Sep 17 00:00:00 2001 From: Gary Oberbrunner Date: Wed, 23 Sep 2026 21:08:38 -0400 Subject: [PATCH 4/4] Support: make the Retimer example ask for the frames it fetches The Retimer example rendered from a source time, the integral of its speed curve or the host's SourceTime value in the retimer context, and fetched the source frames either side of it. Its GetFramesNeeded answer used the output time instead, so at any speed but 1 it told the host it needed frames it did not fetch, and fetched frames it had not asked for: at speed 0.14 it asked for frames 31..32 and fetched frames 4 and 5. Both now compute the source time the same way, in getSourceTime(). Assisted-by: Claude Code / Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Gary Oberbrunner --- Support/Plugins/Retimer/retimer.cpp | 33 +++++++++++++++++------------ release-notes-next.md | 1 + 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Support/Plugins/Retimer/retimer.cpp b/Support/Plugins/Retimer/retimer.cpp index 59f113dd..7b3a502f 100644 --- a/Support/Plugins/Retimer/retimer.cpp +++ b/Support/Plugins/Retimer/retimer.cpp @@ -61,6 +61,9 @@ public : /* override the time domain action, only for the general context */ virtual bool getTimeDomain(OfxRangeD &range); + /* the time on the source clip that the output at time comes from */ + double getSourceTime(double time); + /* set up and run a processor */ void setupAndProcess(OFX::ImageBlenderBase &, const OFX::RenderArguments &args); @@ -120,6 +123,20 @@ static void framesNeeded(double sourceTime, OFX::FieldEnum fieldToRender, double *blendp = blend; } +/* figure the frame we should be retiming from */ +double +RetimerPlugin::getSourceTime(double time) +{ + if(getContext() == OFX::eContextRetimer) { + // the host is specifying it, so fetch it from the kOfxImageEffectRetimerParamName pseudo-param + return sourceTime_->getValueAtTime(time); + } + else { + // we have our own param, which is a speed, so we integrate it to get the time we want + return speed_->integrate(0, time); + } +} + /* set up and run a processor */ void RetimerPlugin::setupAndProcess(OFX::ImageBlenderBase &processor, const OFX::RenderArguments &args) @@ -129,22 +146,10 @@ RetimerPlugin::setupAndProcess(OFX::ImageBlenderBase &processor, const OFX::Rend OFX::BitDepthEnum dstBitDepth = dst->getPixelDepth(); OFX::PixelComponentEnum dstComponents = dst->getPixelComponents(); - // figure the frame we should be retiming from - double sourceTime; - - if(getContext() == OFX::eContextRetimer) { - // the host is specifying it, so fetch it from the kOfxImageEffectRetimerParamName pseudo-param - sourceTime = sourceTime_->getValueAtTime(args.time); - } - else { - // we have our own param, which is a speed, so we integrate it to get the time we want - sourceTime = speed_->integrate(0, args.time); - } - // figure the two images we are blending between double fromTime, toTime; double blend; - framesNeeded(sourceTime, args.fieldToRender, &fromTime, &toTime, &blend); + framesNeeded(getSourceTime(args.time), args.fieldToRender, &fromTime, &toTime, &blend); // fetch the two source images std::unique_ptr fromImg(srcClip_->fetchImage(fromTime)); @@ -177,7 +182,7 @@ RetimerPlugin::getFramesNeeded(const OFX::FramesNeededArguments &args, double fromTime, toTime; double blend; // whatever the rendered field is, the frames are the same - framesNeeded(args.time, OFX::eFieldNone, &fromTime, &toTime, &blend); + framesNeeded(getSourceTime(args.time), OFX::eFieldNone, &fromTime, &toTime, &blend); OfxRangeD range; range.min = fromTime; range.max = toTime; diff --git a/release-notes-next.md b/release-notes-next.md index 0d103bef..8abb5011 100644 --- a/release-notes-next.md +++ b/release-notes-next.md @@ -30,6 +30,7 @@ This is version NEXT of the OpenFX API. - Fixed the Support Noise example giving a different image when a frame is rendered in tiles: it seeded its noise per render call, and now each pixel's noise depends only on its position, the time and the noise level. - Fixed the Support GPUGain example failing to render when the host chose Alpha for its output: it declared Alpha output but processes only RGBA, so its output clip now supports only RGBA. - Fixed the Support Gamma example producing NaNs from negative float input: it now applies the gamma to a value's magnitude and keeps its sign. +- Fixed the Support Retimer example asking for source frames around the output time rather than the retimed source time it then fetches. ## Deprecations