Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix rendering bugs caused by intersecting rects with different mipMapLevels. #918

Merged
merged 3 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions Engine/EffectInstance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ EffectInstance::retrieveGetImageDataUponFailure(const double time,
getRegionsOfInterest(time, scale, optionalBounds, optionalBounds, ViewIdx(0), inputRois_p);
}

assert( !( (supportsRenderScaleMaybe() == eSupportsNo) && !(scale.x == 1. && scale.y == 1.) ) );
assert(isSupportedRenderScale(supportsRenderScaleMaybe(), scale));
const RectI pixelRod = rod.toPixelEnclosing(scale, getAspectRatio(-1));
try {
int identityInputNb;
Expand Down Expand Up @@ -1192,7 +1192,7 @@ EffectInstance::getRegionOfDefinition(U64 hash,
bool firstInput = true;
RenderScale renderMappedScale = scale;

assert( !( (supportsRenderScaleMaybe() == eSupportsNo) && !(scale.x == 1. && scale.y == 1.) ) );
assert(isSupportedRenderScale(supportsRenderScaleMaybe(), scale));

for (int i = 0; i < getNInputs(); ++i) {
if ( isInputMask(i) ) {
Expand Down Expand Up @@ -2402,7 +2402,7 @@ EffectInstance::Implementation::renderHandler(const EffectTLSDataPtr& tls,
actionArgs.byPassCache = byPassCache;
actionArgs.processChannels = processChannels;
actionArgs.mappedScale.x = actionArgs.mappedScale.y = Image::getScaleFromMipMapLevel( firstPlane.renderMappedImage->getMipMapLevel() );
assert( !( (_publicInterface->supportsRenderScaleMaybe() == eSupportsNo) && !(actionArgs.mappedScale.x == 1. && actionArgs.mappedScale.y == 1.) ) );
assert(isSupportedRenderScale(_publicInterface->supportsRenderScaleMaybe(), actionArgs.mappedScale));
actionArgs.originalScale.x = Image::getScaleFromMipMapLevel(mipMapLevel);
actionArgs.originalScale.y = actionArgs.originalScale.x;
actionArgs.draftMode = frameArgs->draftMode;
Expand Down Expand Up @@ -3751,8 +3751,6 @@ EffectInstance::isIdentity_public(bool useIdentityCache, // only set to true whe
ViewIdx* inputView,
int* inputNb)
{
//assert( !( (supportsRenderScaleMaybe() == eSupportsNo) && !(scale.x == 1. && scale.y == 1.) ) );

if (useIdentityCache) {
double timeF = 0.;
bool foundInCache = _imp->actionsCache->getIdentityResult(hash, time, view, inputNb, inputView, &timeF);
Expand Down
8 changes: 8 additions & 0 deletions Engine/EffectInstance.h
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,14 @@ GCC_DIAG_SUGGEST_OVERRIDE_ON
**/
virtual void getFrameRange(double *first, double *last);

/**
* @brief Returns true if the value of renderScale is supported for the given the value of supportsRS.
**/
static bool isSupportedRenderScale(SupportsEnum supportsRS, const RenderScale renderScale)
{
return (supportsRS != eSupportsNo) || (renderScale.x == 1. && renderScale.y == 1.);
}

public:

StatusEnum getRegionOfDefinition_public(U64 hash,
Expand Down
93 changes: 45 additions & 48 deletions Engine/EffectInstanceRenderRoI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ EffectInstance::renderRoI(const RenderRoIArgs & args,
renderMappedMipMapLevel = args.mipMapLevel;
}
RenderScale renderMappedScale( Image::getScaleFromMipMapLevel(renderMappedMipMapLevel) );
assert( !( (supportsRS == eSupportsNo) && !(renderMappedScale.x == 1. && renderMappedScale.y == 1.) ) );
assert(isSupportedRenderScale(supportsRS, renderMappedScale));


const FrameViewRequest* requestPassData = 0;
Expand All @@ -397,26 +397,27 @@ EffectInstance::renderRoI(const RenderRoIArgs & args,

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////// Get the RoD ///////////////////////////////////////////////////////////////
RectD rod; //!< rod is in canonical coordinates

RectD rodNc; //!< rod is in canonical coordinates
bool isProjectFormat = false;
{
///if the rod is already passed as parameter, just use it and don't call getRegionOfDefinition
if ( !args.preComputedRoD.isNull() ) {
rod = args.preComputedRoD;
rodNc = args.preComputedRoD;
} else {
///Check if the pre-pass already has the RoD
if (requestPassData) {
rod = requestPassData->globalData.rod;
rodNc = requestPassData->globalData.rod;
isProjectFormat = requestPassData->globalData.isProjectFormat;
} else {
assert( !( (supportsRS == eSupportsNo) && !(renderMappedScale.x == 1. && renderMappedScale.y == 1.) ) );
StatusEnum stat = getRegionOfDefinition_public(nodeHash, args.time, renderMappedScale, args.view, &rod, &isProjectFormat);
assert(isSupportedRenderScale(supportsRS, renderMappedScale));
StatusEnum stat = getRegionOfDefinition_public(nodeHash, args.time, renderMappedScale, args.view, &rodNc, &isProjectFormat);

///The rod might be NULL for a roto that has no beziers and no input
if (stat == eStatusFailed) {
///if getRoD fails, this might be because the RoD is null after all (e.g: an empty Roto node), we don't want the render to fail
return eRenderRoIRetCodeOk;
} else if ( rod.isNull() ) {
} else if ( rodNc.isNull() ) {
//Nothing to render
return eRenderRoIRetCodeOk;
}
Expand All @@ -432,18 +433,9 @@ EffectInstance::renderRoI(const RenderRoIArgs & args,
}
}
}
const RectD rod = rodNc;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////// End get RoD ///////////////////////////////////////////////////////////////
RectI roi;
{
if (renderFullScaleThenDownscale) {
//We cache 'image', hence the RoI should be expressed in its coordinates
//renderRoIInternal should check the bitmap of 'image' and not downscaledImage!
roi = args.roi.toNewMipMapLevel(args.mipMapLevel, 0, par, rod);
} else {
roi = args.roi;
}
}

///Determine needed planes
ComponentsNeededMapPtr neededComps = std::make_shared<ComponentsNeededMap>();
Expand Down Expand Up @@ -553,7 +545,7 @@ EffectInstance::renderRoI(const RenderRoIArgs & args,
double inputTimeIdentity = 0.;
int inputNbIdentity;
ViewIdx inputIdentityView(args.view);
assert( !( (supportsRS == eSupportsNo) && !(renderMappedScale.x == 1. && renderMappedScale.y == 1.) ) );
assert(isSupportedRenderScale(supportsRS, renderMappedScale));
bool identity;
const RectI pixelRod = rod.toPixelEnclosing(args.mipMapLevel, par);
ViewInvarianceLevel viewInvariance = isViewInvariant();
Expand Down Expand Up @@ -736,39 +728,44 @@ EffectInstance::renderRoI(const RenderRoIArgs & args,
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////// Compute RoI depending on render scale ///////////////////////////////////////////////////


RectI downscaledImageBoundsNc = rod.toPixelEnclosing(args.mipMapLevel, par);
RectI upscaledImageBoundsNc = rod.toPixelEnclosing(0, par);
{
///Make sure the RoI falls within the image bounds
///Intersection will be in pixel coordinates
if (frameArgs->tilesSupported) {
if (renderFullScaleThenDownscale) {
if ( !roi.clipIfOverlaps(upscaledImageBoundsNc) ) {
return eRenderRoIRetCodeOk;
}
assert(roi.x1 >= upscaledImageBoundsNc.x1 && roi.y1 >= upscaledImageBoundsNc.y1 &&
roi.x2 <= upscaledImageBoundsNc.x2 && roi.y2 <= upscaledImageBoundsNc.y2);
} else {
if ( !roi.clipIfOverlaps(downscaledImageBoundsNc) ) {
return eRenderRoIRetCodeOk;
}
assert(roi.x1 >= downscaledImageBoundsNc.x1 && roi.y1 >= downscaledImageBoundsNc.y1 &&
roi.x2 <= downscaledImageBoundsNc.x2 && roi.y2 <= downscaledImageBoundsNc.y2);
}
#ifndef NATRON_ALWAYS_ALLOCATE_FULL_IMAGE_BOUNDS
///just allocate the roi
upscaledImageBoundsNc.clipIfOverlaps(roi);
downscaledImageBoundsNc.clipIfOverlaps(args.roi);
#endif
RectI roi;

if (renderFullScaleThenDownscale) {
//We cache 'image', hence the RoI should be expressed in its coordinates
//renderRoIInternal should check the bitmap of 'image' and not downscaledImage!
roi = args.roi.toNewMipMapLevel(args.mipMapLevel, 0, par, rod);

if (frameArgs->tilesSupported && !roi.clipIfOverlaps(upscaledImageBoundsNc)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's good practice to have the condition actually modify the context. I think we should move all clipIfOverlaps() calls out of the conditions.
This condition would then look like:

if (frameArgs->tilesSupported) {
  auto clipped = roi.clipIfOverlaps(upscaledImageBoundsNc);
  if (!clipped) {
     return eRenderRoIRetCodeOk;
   }
}

but I'm not sure that's the intent here, because a few lines below there's const RectI originalRoI = roi; but roiis not the original but the clipped roi... what do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If roi was used elsewhere in the conditional I might agree, but it isn't so I disagree with the proposed substitute. In my opinion the proposal is unnecessarily verbose and it assigns a name to a value that really is only needed for the conditional that follows. In my experience, assigning names to values is usually a signal that they will be used more than once, but in cases like this it is not true which is why I don't really see the need for giving it a name.

Unfortunately I don't know the history of why the variable below is called originalRoI, but I suspect it is intended to store the value of roi before any further modifications occur after that point. I'm not sure what a better name might be given the complexity of this function and how clipping is applied(or not) based on a variety of conditions. I too was initially confused by the use of the term 'original' given all the possible roi transformations that occur before that declaration.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think side effects in conditions are bad practice in general (except for some constructs where the language instructs you to use side effects like for or while), because a condition should check the state, not modify it. This makes the code error-prone and harder to read.

return eRenderRoIRetCodeOk;
}
assert( upscaledImageBoundsNc.contains(roi));
} else {
roi = args.roi;

if (frameArgs->tilesSupported && !roi.clipIfOverlaps(downscaledImageBoundsNc)) {
return eRenderRoIRetCodeOk;
}
assert(downscaledImageBoundsNc.contains(roi));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attach any input to viewer (and/or scale) and assert fail.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by #930 . I accidentally made the assert apply to the case where tiles are not supported, which does not match the original code. I've restored the old behavior so the assert only applies when tiles are supported.

}

/*
* Keep in memory what the user as requested, and change the roi to the full bounds if the effect doesn't support tiles
* Keep in memory what the user has requested, and change the roi to the full bounds if the effect doesn't support tiles
*/
const RectI originalRoI = roi;
if (!frameArgs->tilesSupported) {
// Store the mipMapLevel for originalRoI here because renderFullScaleThenDownscale may change later and we'll lose
// the ability to recover this information when we potentially need it for a downscale operation later.
const unsigned int originalRoIMipMapLevel = renderFullScaleThenDownscale ? 0 : args.mipMapLevel;

if (frameArgs->tilesSupported) {
#ifndef NATRON_ALWAYS_ALLOCATE_FULL_IMAGE_BOUNDS
///just allocate the roi
const RectI upscaledRoI = renderFullScaleThenDownscale ? roi : roi.toNewMipMapLevel(args.mipMapLevel, 0, par, rod);
upscaledImageBoundsNc.clipIfOverlaps(upscaledRoI);
downscaledImageBoundsNc.clipIfOverlaps(args.roi);
#endif
} else {
roi = renderFullScaleThenDownscale ? upscaledImageBoundsNc : downscaledImageBoundsNc;
}

Expand Down Expand Up @@ -1199,10 +1196,9 @@ EffectInstance::renderRoI(const RenderRoIArgs & args,
if (!input) {
continue;
}
bool isProjectFormat;
ParallelRenderArgsPtr inputFrameArgs = input->getParallelRenderArgsTLS();
U64 inputHash = (inputFrameArgs) ? inputFrameArgs->nodeHash : input->getHash();
StatusEnum stat = input->getRegionOfDefinition_public(inputHash, args.time, args.scale, args.view, &inputRod, &isProjectFormat);
StatusEnum stat = input->getRegionOfDefinition_public(inputHash, args.time, args.scale, args.view, &inputRod, nullptr);
if ( (stat != eStatusOK) && !inputRod.isNull() ) {
break;
}
Expand Down Expand Up @@ -1757,7 +1753,8 @@ EffectInstance::renderRoI(const RenderRoIArgs & args,
assert(comp);
///The image might need to be converted to fit the original requested format
if (comp) {
it->second.downscaleImage = convertPlanesFormatsIfNeeded(getApp(), it->second.downscaleImage, originalRoI, *comp, args.bitdepth, useAlpha0ForRGBToRGBAConversion, planesToRender->outputPremult, -1);
const RectI downscaledOriginalRoI = originalRoI.toNewMipMapLevel(originalRoIMipMapLevel, args.mipMapLevel, par, rod);
it->second.downscaleImage = convertPlanesFormatsIfNeeded(getApp(), it->second.downscaleImage, downscaledOriginalRoI, *comp, args.bitdepth, useAlpha0ForRGBToRGBAConversion, planesToRender->outputPremult, -1);
assert(it->second.downscaleImage->getComponents() == *comp && it->second.downscaleImage->getBitDepth() == args.bitdepth);

StorageModeEnum imageStorage = it->second.downscaleImage->getStorageMode();
Expand Down Expand Up @@ -1909,7 +1906,7 @@ EffectInstance::renderRoIInternal(EffectInstance* self,


if (callBegin) {
assert( !( (self->supportsRenderScaleMaybe() == eSupportsNo) && !(renderMappedScale.x == 1. && renderMappedScale.y == 1.) ) );
assert( self->isSupportedRenderScale(self->supportsRenderScaleMaybe(), renderMappedScale) );
if (self->beginSequenceRender_public(time, time, 1, !appPTR->isBackground(), renderMappedScale, isSequentialRender,
isRenderMadeInResponseToUserInteraction, frameArgs->draftMode, view, planesToRender->useOpenGL, planesToRender->glContextData) == eStatusFailed) {
renderStatus = eRenderingFunctorRetFailed;
Expand Down Expand Up @@ -1997,7 +1994,7 @@ EffectInstance::renderRoIInternal(EffectInstance* self,

///never call endsequence render here if the render is sequential
if (callBegin) {
assert( !( (self->supportsRenderScaleMaybe() == eSupportsNo) && !(renderMappedScale.x == 1. && renderMappedScale.y == 1.) ) );
assert( self->isSupportedRenderScale(self->supportsRenderScaleMaybe(), renderMappedScale) );
if (self->endSequenceRender_public(time, time, time, false, renderMappedScale,
isSequentialRender,
isRenderMadeInResponseToUserInteraction,
Expand Down
21 changes: 6 additions & 15 deletions Engine/OfxEffectInstance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1598,7 +1598,7 @@ OfxEffectInstance::getRegionsOfInterest(double time,
Q_UNUSED(outputRoD);
assert(renderWindow.x2 >= renderWindow.x1 && renderWindow.y2 >= renderWindow.y1);

assert((supportsRenderScaleMaybe() != eSupportsNo) || (scale.x == 1. && scale.y == 1.));
assert(isSupportedRenderScale(supportsRenderScaleMaybe(), scale));

OfxStatus stat;

Expand Down Expand Up @@ -1828,11 +1828,10 @@ OfxEffectInstance::isIdentity(double time,
// isIdentity may be the first action with renderscale called on any effect.
// it may have to check for render scale support.
SupportsEnum supportsRS = supportsRenderScaleMaybe();
bool scaleIsOne = (scale.x == 1. && scale.y == 1.);
if ( (supportsRS == eSupportsNo) && !scaleIsOne ) {
qDebug() << "isIdentity called with render scale != 1, but effect does not support render scale!";
if (!isSupportedRenderScale(supportsRS, scale) ) {
qDebug() << "isIdentity called with an unsupported RenderScale!";
assert(false);
throw std::logic_error("isIdentity called with render scale != 1, but effect does not support render scale!");
throw std::logic_error("isIdentity called with an unsupported RenderScale");
}

unsigned int mipMapLevel = Image::getLevelFromScale(scale.x);
Expand Down Expand Up @@ -1944,11 +1943,7 @@ OfxEffectInstance::beginSequenceRender(double first,
bool isOpenGLRender,
const EffectInstance::OpenGLContextEffectDataPtr& glContextData)
{
{
bool scaleIsOne = (scale.x == 1. && scale.y == 1.);
assert( !( (supportsRenderScaleMaybe() == eSupportsNo) && !scaleIsOne ) );
Q_UNUSED(scaleIsOne);
}
assert(isSupportedRenderScale(supportsRenderScaleMaybe(), scale));

OfxStatus stat;
unsigned int mipMapLevel = Image::getLevelFromScale(scale.x);
Expand Down Expand Up @@ -1990,11 +1985,7 @@ OfxEffectInstance::endSequenceRender(double first,
bool isOpenGLRender,
const EffectInstance::OpenGLContextEffectDataPtr& glContextData)
{
{
bool scaleIsOne = (scale.x == 1. && scale.y == 1.);
assert( !( (supportsRenderScaleMaybe() == eSupportsNo) && !scaleIsOne ) );
Q_UNUSED(scaleIsOne);
}
assert(isSupportedRenderScale(supportsRenderScaleMaybe(), scale));

OfxStatus stat;
unsigned int mipMapLevel = Image::getLevelFromScale(scale.x);
Expand Down