hi there,
when we draw a lot of stuff in our plugin and drawings are cached in layers, and controls are only repainted when changes are made to them, we can draw a quite complex gui without having a big impact on the framerate. however when we rescale our plugin, all controls are repainted at the same time, which can substantially decrease fps on rescaling.
to solve this, an idea is to paint the layers of the already cached controls fitted (igraphics::drawfittedlayer) on rescaling. so instead of invalidating the layers on rescale, we keep them and paint them fitted into the new bounds. then when rescaling is done, all layers can be repainted once, to draw everything clear again.
a downside to this is, that when the plugin size is increased the cached layers start to appear blurry or out of proportion, as for example if we may paint a 100x100px layer in maybe 300x200px. to keep the gui good looking while rescaling, we can do the following: on rescaling, all layers get drawn fitted, and each control gets a different repaint starting point. so for our controls, each layer gets redrawn just every x frames, and each one of them starts repainting in a different frame - this way we spread the load across multiple frames, and depending on how many controls we have we may only need to repaint a few controls per frame, while the others get drawn fitted. this is a good compromise for freeing up a lot of fps while keeping the looks of the gui quite good while rescaling.
heres a video showing the below code example with explanations:
https://odysee.com/iplug2-better-framerates-on-plugin-resize:1
to test it out replace IPlugEffect.h and IPlugEffect.cpp in examples/IPlugEffect with the following:
IPlugEffect.h :
#pragma once
#include "IPlug_include_in_plug_hdr.h"
#include "IControl.h"
#include <string>
#include <iostream>
#include <sstream>
#include <iomanip>
using namespace std;
using namespace iplug;
using namespace igraphics;
const int kNumPresets = 1;
struct Point
{
Point(){};
Point(double x, double y)
: x(x)
, y(y)
{
}
double x;
double y;
};
class MyControl
{
public:
class GUI;
GUI* gui = nullptr;
IRECT CR; // ControlRect
IRECT LBR; //LayerButtonRect
IRECT DBR; //DetailButtonRect
IRECT IPBR; //IntervalPaintButtonRect
bool UseLayer = true;
bool ResizeIntervalRePaint = true;
int ResizeIntervalRepaintCounter = 0;
int IDX = -1;
bool GuiIsResing = false;
double Detail = 0.5; //Gui Load
MyControl() {}
void Construct(IRECT R, int idx = -1)
{
CR = R;
IDX = idx;
}
void AttachControl(IGraphics& G) { G.AttachControl(new GUI(this, CR, IDX)); }
void SetDetail(float y)
{
Detail = 1. - (y - DBR.T) / DBR.H();
if (Detail < 0)
Detail = 0;
else if (Detail > 1)
Detail = 1;
}
void DrawControl(IGraphics& G, const IRECT R, IColor color, int detail = 100) {
detail = BoundNum(detail, 1, R.GetLengthOfShortestSide());
int cells = R.GetLengthOfShortestSide() / (R.GetLengthOfShortestSide() / detail);
IRECT C;
Point RC = RectGetPoint(R, 0.5, 0.5); // RectCenter
float Diag = GetPointDistance(RC, RectGetPoint(R, 0, 0));
for (int ch = 0; ch < cells; ch++)
{
for (int cv = 0; cv < cells; cv++)
{
C = RectGetTableCell(R, cells, cells, ch, cv);
Point CC = RectGetPoint(C, 0.5, 0.5); //CellCenter
float PD = GetPointDistance(RC, CC);
float CF = Normalize(0, Diag, PD); //ColorFactor
IColor NewC = AdjustColor(color, 1.-CF);
G.FillRect(NewC, C);
}
}
IText T;
T.mSize = R.H() * 0.1;
T.mFGColor = COLOR_GRAY;
IRECT TB;
G.MeasureText(T, "some text to observe sharpness", TB);
T.mSize *= R.W() / TB.W() * 0.7;
G.MeasureText(T, "some text to observe sharpness", TB);
TB = RectMove(TB, -TB.L + R.W() / 2 - TB.W() / 2, -TB.T + R.H() / 2);
G.FillRect(COLOR_BLACK, TB);
G.DrawText(T, "some text to observe sharpness", TB);
}
class GUI : public IControl
{
public:
// here you can access the gui and mouseinteractions of the control. dont store data here, as this control gets deleted when gui is closed. use the data stored in parentclass(MyControl) and access
// it via MyControl* MC :-)
MyControl* MC = nullptr;
ILayerPtr Lay;
GUI(MyControl* myControl, IRECT r, int idx = -1)
: IControl(r, idx)
{
MC = myControl;
};
void Draw(IGraphics& G) override
{
if (this->GetRECT() != MC->CR)
{
this->SetTargetAndDrawRECTs(MC->CR); // ensures that the icontrol rect follows the IRECT CR in MyControl
}
//Updating Control Rects
MC->LBR = MC->RectGetTableCell(MC->CR, 5, 10, 4, 0);
MC->DBR = MC->RectGetTableCell(MC->CR, 10, 2, 0, 1);
MC->IPBR = MC->RectGetTableCell(MC->CR, 5, 10, 3, 0);
if (MC->UseLayer)
{
if (MC->GuiIsResing && !G.GetResizingInProcess()) //when resizing stopped repaint layer
Lay->Invalidate();
if (MC->ResizeIntervalRePaint && G.GetResizingInProcess() && MC->ResizeIntervalRepaintCounter > 10) // on resize repaint layer every x frame
{
Lay->Invalidate();
MC->ResizeIntervalRepaintCounter = 0;
}
if (!G.CheckLayer(Lay))
{
G.StartLayer(nullptr, MC->CR, true);
MC->DrawControl(G, MC->CR, IColor::GetRandomColor(), MC->CR.GetLengthOfShortestSide() / MC->BoundNum(100 * (1. - MC->Detail), 1, MC->CR.GetLengthOfShortestSide()));
Lay = G.EndLayer();
SetDirty(true);
}
G.DrawFittedLayer(Lay, MC->CR, nullptr);
}
else
{
MC->DrawControl(G, MC->CR, IColor::GetRandomColor(), MC->CR.GetLengthOfShortestSide() / MC->BoundNum(100 * (1. - MC->Detail), 1, MC->CR.GetLengthOfShortestSide()));
}
MC->GuiIsResing = G.GetResizingInProcess();
++MC->ResizeIntervalRepaintCounter;
//Drawing the Controls
G.FillRect(IColor::GetRandomColor(), MC->LBR);
string LStr = "Layer On";
if (!MC->UseLayer)
LStr = "Layer Off";
G.DrawText(IText(), LStr.c_str(), MC->LBR);
G.FillRect(COLOR_GRAY, MC->DBR);
IRECT DL = {MC->DBR.L, MC->DBR.B - MC->DBR.H() * float(MC->Detail), MC->DBR.R, MC->DBR.B};
G.FillRect(COLOR_ORANGE, DL);
G.DrawText(IText(), "GUI Load", MC->DBR);
if (MC->ResizeIntervalRePaint)
{
G.FillRect(COLOR_ORANGE, MC->IPBR);
LStr = "IntervalRepaint ON";
}
else
{
G.FillRect(COLOR_GRAY, MC->IPBR);
LStr = "intervalRePaint OFF";
}
G.DrawText(IText(), LStr.c_str(), MC->IPBR);
}
bool AdjustDetail = false;
void OnMouseDown(float x, float y, const IMouseMod& Mod) override
{
if (MC->XYIsInRect(MC->LBR, x, y))
MC->UseLayer = !MC->UseLayer;
else if (MC->XYIsInRect(MC->IPBR, x, y))
MC->ResizeIntervalRePaint = !MC->ResizeIntervalRePaint;
else if (MC->XYIsInRect(MC->DBR, x, y))
{
MC->SetDetail(y);
AdjustDetail = true;
}
Lay->Invalidate();
SetDirty(true);
}
void OnMouseUp(float x, float y, const IMouseMod& Mod) override
{
if (AdjustDetail)
MC->SetDetail(y);
Lay->Invalidate();
SetDirty(true);
AdjustDetail = false;
}
void OnMouseDrag(float x, float y, float dx, float dy, const IMouseMod& Mod) override
{
if (AdjustDetail)
MC->SetDetail(y);
Lay->Invalidate();
SetDirty(true);
}
void OnMouseOver(float x, float y, const IMouseMod& Mod) override {}
void OnMouseDblClick(float x, float y, const IMouseMod& Mod) override {}
void OnMouseWheel(float x, float y, const IMouseMod& Mod, float d) override{};
void OnResize() override {}
void OnMouseOut() override {}
virtual void OnTextEntryCompletion(const char* str, int valIdx) {}
//bool IsDirty() override { return true;
//}
~GUI() { MC->gui = nullptr; }
};
private:
bool XYIsInRect(const IRECT& pRect, double x, double y)
{
// # XYIsInRect
if (x >= pRect.L && x <= pRect.R && y >= pRect.T && y <= pRect.B)
return true;
else
return false;
}
IRECT RectGetSliced(const IRECT& R, bool HorizontalSlicing, int NumberOfSlices, int ReturnSliceNumber)
{
// # RectGetSliced
if (HorizontalSlicing)
{
return IRECT(R.L, R.T + (R.H() / NumberOfSlices) * ReturnSliceNumber, R.R, R.T + (R.H() / NumberOfSlices) * (ReturnSliceNumber + 1));
}
else
{
return IRECT(R.L + (R.W() / NumberOfSlices) * ReturnSliceNumber, R.T, R.L + (R.W() / NumberOfSlices) * (ReturnSliceNumber + 1), R.B);
}
}
IRECT RectGetTableCell(const IRECT& R, int NumColumns, int NumRows, int GetColumn, int GetRow)
{
// # RectGetTableCell
return RectGetSliced(RectGetSliced(R, true, NumRows, GetRow), false, NumColumns, GetColumn);
}
float GetPointDistance(Point& p1, Point& p2)
{
// # GetPointDistance
return sqrt(pow(p2.x - p1.x, 2) + pow(p2.y - p1.y, 2) * 1.0);
}
Point RectGetPoint(const IRECT& R, double xGrab, double yGrab)
{
// # RectGetPoint
return Point(R.L + (R.W() * xGrab), R.T + (R.H() * yGrab));
}
double Normalize(double min, double max, double value)
{
// # Normalize
return (value - min) / (max - min);
}
double UnNormalize(double min, double max, double normalized)
{
// # UnNormalize
return normalized * (max - min) + min;
}
double BoundNum(double Num, double BoundA, double BoundB)
{
// # BoundNum
double Min = min(BoundA, BoundB);
if (Num < Min)
return Min;
double Max = max(BoundA, BoundB);
if (Num > Max)
return Max;
return Num;
}
IRECT RectMove(const IRECT& r, double xDelta, double yDelta)
{
// # RectMove
return IRECT(r.L + xDelta, r.T + yDelta, r.R + xDelta, r.B + yDelta);
}
inline IColor AdjustColor(const IColor& c, float perc)
{
// # AdjustColor
IColor C = IColor(c);
C.R = BoundNum(C.R * perc, 0, 255);
C.G = BoundNum(C.G * perc, 0, 255);
C.B = BoundNum(C.B * perc, 0, 255);
return C;
}
};
enum EParams
{
kGain = 0,
kNumParams
};
class IPlugEffect final : public Plugin
{
public:
IPlugEffect(const InstanceInfo& info);
MyControl MYC;
#if IPLUG_DSP // http://bit.ly/2S64BDd
void ProcessBlock(iplug::sample** inputs, iplug::sample** outputs, int nFrames) override;
bool IPlugEffect::EditorResizeFromUI(int viewWidth, int viewHeight, bool needsPlatformResize) override;
#endif
};
IPlugEffect.cpp
#include "IPlugEffect.h"
#include "IPlug_include_in_plug_src.h"
#include "IControls.h"
IPlugEffect::IPlugEffect(const InstanceInfo& info)
: Plugin(info, MakeConfig(kNumParams, kNumPresets))
{
GetParam(kGain)->InitDouble("Gain", 0., 0., 100.0, 0.01, "%");
MYC.Construct({0, 0, PLUG_WIDTH, PLUG_HEIGHT});
#if IPLUG_EDITOR // http://bit.ly/2S64BDd
mMakeGraphicsFunc = [&]() {
return MakeGraphics(*this, PLUG_WIDTH, PLUG_HEIGHT, PLUG_FPS, GetScaleForScreen(PLUG_WIDTH, PLUG_HEIGHT));
};
mLayoutFunc = [&](IGraphics* pGraphics) {
pGraphics->AttachCornerResizer(EUIResizerMode::Size, false);
// pGraphics->AttachPanelBackground(COLOR_GRAY);
pGraphics->LoadFont("Roboto-Regular", ROBOTO_FN);
const IRECT b = pGraphics->GetBounds();
pGraphics->ShowFPSDisplay(true);
MYC.CR = b;
MYC.AttachControl(*pGraphics);
};
#endif
}
#if IPLUG_DSP
void IPlugEffect::ProcessBlock(iplug::sample** inputs, iplug::sample** outputs, int nFrames)
{
const double gain = GetParam(kGain)->Value() / 100.;
const int nChans = NOutChansConnected();
for (int s = 0; s < nFrames; s++) {
for (int c = 0; c < nChans; c++) {
outputs[c][s] = inputs[c][s] * gain;
}
}
}
bool IPlugEffect::EditorResizeFromUI(int viewWidth, int viewHeight, bool needsPlatformResize)
{
bool r = EditorResize(viewWidth, viewHeight);
float scale = 1. / GetUI()->GetPlatformWindowScale();
float W = viewWidth * scale;
float H = viewHeight * scale;
MYC.CR = IRECT(0, 0, W, H);
if (needsPlatformResize)
return r;
else
return true;
}
#endif