Closed DinkydauSet closed 3 years ago
What is currently required to add a procedure:
Then, in order to actually use the procedure, a menu option has to be added. It's also possible to edit the JSON and change the identifier there to change the procedure.
I now understand what the problem is with those templates. I declare my formulas as "const" and I expected to be able to use those values as template parameters, and then found out that I can't. The problem is a misunderstanding of what const means.
Const only means the value is initialized once and then never changed. It doesn't mean the value is known at compile time.
What I needed is a constexpr. A constexpr is known at compile time and can be used as a template parameter.
Unfortunately std::string can't be a constexpr, which is about to change in c++20 but my visual studio and the latest gcc don't have that feature yet so I can't declare a Formula as constexpr just because there's a string in there.
This person on stackoverflow has a solution: https://stackoverflow.com/a/37876799 . A small test program:
#include <string>
//from https://stackoverflow.com/a/37876799
struct constexpr_str {
char const* str;
std::size_t size;
// can only construct from a char[] literal
template <std::size_t N>
constexpr constexpr_str(char const (&s)[N])
: str(s)
, size(N - 1) // not count the trailing nul
{}
};
class properties {
public:
int i;
constexpr_str name;
std::string name_() {
return std::string(name.str);
}
};
constexpr properties p = { 7, "songnrso" };
template <int i>
int integer() { return i; }
int main() {
return integer<p.i>();
}
The properties class can be declared as a constexpr and hence the member i can be used as a template parameter.
This approach can be used for the Formula objects, and then I will be able to keep formula properties in one place. The template parameters can be chosen something like this:
case M2.identifier: {
if (using_avx) {
if (S.get_julia()) {
createNewRenderTemplated<M2.identifier, M2.guessing, M2.avx, true>(headless);
}
...
Another benefit that I expect with this is that it doesn't matter if unnecessary cases are added, such as
createNewRenderTemplated<M2.identifier, M2.guessing, false, true>(headless);
to use no AVX if M2 doesn't even have an AVX implementation. Then M2.avx is false and both templates will have the same parameters, leading the only one function be compiled and included in the binary. There will be no bloat with unused functions.
Have you considered using one type per formula that contains its functions as well as properties, and using that type as template argument? That way you can keep both the formula and its properties in one place.
A brief example to show the concept:
#include <string>
#include <iostream>
template<typename FormulaT>
class Render {
public:
void somethingThatCouldGuess() const {
if constexpr(FormulaT::is_guessable) { // note that `if constexpr` is a C++17 feature
// make a guess
}
else {
// do something else
}
}
unsigned int calcPoint(unsigned int x, unsigned int y) const {
std::cout << "Calculating point using formula \"" << FormulaT::name << "\"..." << std::endl;
return FormulaT::calcPoint(x, y);
}
};
struct ProcedureM2_Formula { // Separate formula types would be defined as one unique struct (similar to this one) per formula.
constexpr static int identifier = 4;
constexpr static bool is_guessable = true;
constexpr static int inflection_power = 2;
constexpr static bool is_escape_time = true;
constexpr static bool escape_radius = true;
const static std::string name;
static unsigned int calcPoint(unsigned int x, unsigned int y) {
// TODO: actual code here
return x + y;
}
};
const std::string ProcedureM2_Formula::name = "Mandelbrot power 2"; // has to be initialized out-of-line
// Usage example:
int main() {
Render<ProcedureM2_Formula> render; // instantiate `Render` with `ProcedureM2_Formula`
// prints "Calculating point using formula "Mandelbrot power 2"..." and then "3"
std::cout << render.calcPoint(1, 2) << std::endl;
return 0;
}
Thanks for your ideas.
Yes, I have considered using a class for each formula but that was before I knew how to use templates. Knowing what I know now, that may be a good idea. I'll think about it again.
I have also considered placing the calcPoint function of each formula in the class, but there's a problem with that:
calcPoint needs more information than just x and y. For Mandelbrot power 2 the x and y-coordinate on the screen need to be mapped onto a complex number. This is the map function of the FractalParameters class:
inline double_c map(uint xPos, uint yPos) {
assert(xPos >= 0); assert(xPos <= get_width());
assert(yPos >= 0); assert(yPos <= get_height());
return get_topleftCorner() + xPos * get_pixelWidth() - yPos * get_pixelHeight()*I;
}
It uses the complex number in the top left corner of the screen and the pixel width and -height. Those values are stored in FractalParameters. There's more than just that. To do what I made the program for (Julia morphings) the Julia morphings need to be applied to the complex number before applying the Mandelbrot formula, and the coordinates of the Julia morphings are also stored in FractalParameters (in the vector inflectionCoords).
So the next idea that I had was: let the Render class apply all those mappings and keep only the Mandelbrot formula as a function in the ProcedureM2_Formula class. Then it would be something like this:
static unsigned int calcPoint(double_c c) {
int iterationCount = 0;
double_c z = 0;
while (z has not escaped) {
z = pow(z, 2) + c;
iterationCount++;
}
return iterationCount;
}
That could work, but not as a general solution because not all formulas need the same mapping of x and y onto a complex number. I would still need if-else-if-else... somewhere in the Render class to apply the right mapping for each formula. which made me wonder: what was the point of trying to move the calculation to a class?
I've started to like the calcPoint function as it is, having all the calculations in once place, from x and y coordinates to the final result. It's also very general. I could use it to render something completely different. The only restriction is that the pixels need to be computable independently.
I made a struct for each escape time formula because then I can save the escape radius in those structs. Now the other formulas don't have an escape radius anymore, as it should be.
template <int procedure_identifier>
struct escapeTimeFormula {
static constexpr double escapeRadius = 0;
static double_c apply(double_c z, double_c c) { assert(false); return 0; }; //default implementation that should not be used
};
template <>
struct escapeTimeFormula<M3.identifier> {
static constexpr double escapeRadius = 2;
static double_c apply(double_c z, double_c c) {
return pow(z, 3) + c;
}
};
...more structs
I renamed everything else related to formulas to procedures. Formula is now called Procedure.
Render doesn't have the guessing parameter in its template anymore because it can take the value from the procedure object beloning to the procedure_identifier:
template <int procedure_identifier, bool use_avx, bool julia>
class Render {
public:
const uint renderID;
FractalCanvas& canvas;
static constexpr Procedure procedure = getProcedureObject(procedure_identifier); //this value is known at compile time
Where guessing is needed, it's available through procedure.guessable
and finally, I use the constexpr values in the Procedure objects as template parameters:
#define procedureRenderCase(procedure_identifier) \
case procedure_identifier: { \
constexpr Procedure procedure = getProcedureObject(procedure_identifier); \
if (using_avx) { \
if (S.get_julia()) \
createNewRenderTemplated<procedure_identifier, procedure.hasAvxVersion, procedure.hasJuliaVersion>(headless); \
else \
createNewRenderTemplated<procedure_identifier, procedure.hasAvxVersion, false>(headless); \
} \
else { \
if (S.get_julia()) \
createNewRenderTemplated<procedure_identifier, false, procedure.hasJuliaVersion>(headless); \
else \
createNewRenderTemplated<procedure_identifier, false, false>(headless); \
} \
break; \
}
/* createNewRenderTemplated has template:
template <int procedure_identifier, bool use_avx, bool julia>
*/
void FractalCanvas::createNewRender(bool headless) {
if(debug) {
int procedure_identifier = S.get_procedure_identifier();
assert(procedure_identifier == S.get_procedure().identifier);
cout << "creating new render with procedure: " << procedure_identifier << " (" << S.get_procedure().name() << ")" << endl;
}
switch (S.get_procedure_identifier()) {
procedureRenderCase(M2.identifier)
procedureRenderCase(M3.identifier)
procedureRenderCase(M4.identifier)
procedureRenderCase(M5.identifier)
procedureRenderCase(BURNING_SHIP.identifier)
procedureRenderCase(CHECKERS.identifier)
procedureRenderCase(TRIPLE_MATCHMAKER.identifier)
procedureRenderCase(HIGH_POWER.identifier)
procedureRenderCase(RECURSIVE_FRACTAL.identifier)
procedureRenderCase(BI.identifier)
procedureRenderCase(PURE_MORPHINGS.identifier)
}
}
This causes all combinations of procedure_identifier, use_avx and julia to be generated by the macro. If a procedure doesn't have an avx version or julia version, some of those template parameter combinations are the same. I notice that the size of the exe decreased after doing this so it has the effect that I wanted.
These are now the steps to add a new procedure:
It's still not really simple but I have no idea how it can be made simpler.
List of all procedure definitions:
// id guessable inflection- name hasJulia- hasAvxVersion
// Power Version
constexpr Procedure NOT_FOUND = {-1, false, 0, "Procedure not found", false, false };
constexpr Procedure M2 = { 4, true, 2, "Mandelbrot power 2", true, true };
constexpr Procedure M3 = { 6, true, 3, "Mandelbrot power 3", true, false };
constexpr Procedure M4 = { 7, true, 4, "Mandelbrot power 4", true, false };
constexpr Procedure M5 = { 8, true, 5, "Mandelbrot power 5", true, false };
constexpr Procedure BURNING_SHIP = { 5, false, 2, "Burning ship", true, false };
constexpr Procedure CHECKERS = { 12, true, 2, "Checkers", false, false };
constexpr Procedure TRIPLE_MATCHMAKER = { 11, true, 2, "Triple Matchmaker", true, false };
constexpr Procedure HIGH_POWER = { 13, true, 2, "High power Mandelbrot",true, false };
constexpr Procedure RECURSIVE_FRACTAL = { 15, true, 2, "Recursive Fractal", false, false };
constexpr Procedure BI = { 16, true, 2, "Business Intelligence",false, false };
constexpr Procedure PURE_MORPHINGS = { 17, true, 2, "Pure Julia morphings", false, false };
By a procedure I mean what is usually a fractal type. In general it's a procedure that turns a pixel coordinate in an iteration count, which could be abused to render all kinds of stuff. It doesn't have to be a fractal.
Some properties of procedures are stored in multiple locations, for example whether they can use guessing. That property is inserted in the templates here:
and also in the definition of the Formula objects:
Maybe I should get rid of Formula objects altogether. I thought it would be useful to have one object with properties for each formula/procedure in the program. The templates ruin that idea. Also I have a hardcoded escape radius of 4 for Mandelbrot power 2 in the Render class:
and for some formulas the escape radius is not applicable, so the construction with Formula objects isn't as nice as I hoped.