Skip to content

Latest commit

 

History

History
548 lines (412 loc) · 12 KB

TRANSFORMATIONS.md

File metadata and controls

548 lines (412 loc) · 12 KB

Transformations

Here is a list of transformations that Shader Minifier applies to the shader code.

Some of the transformations are not completely safe and may break in some corner cases. If you observe differences, don't hesitate to report a bug.

Whitespace and comments removal

All comments are removed. Whitespace is removed whenever possible.

This transformation cannot be disabled, although you may use --format indented to see an indented output.

Parentheses simplifications

Parentheses that are not needed are automatically removed. This transformations cannot be disabled; parentheses are not even recorded in the AST, they are computed based on operator precedence rules.

Input:

int a = (x * x) * (x + 1);

Output:

int a = x * x * (x + 1);

Curly braces removal

Curly braces are removed when they are not required. This happens when a block contains a single statement:

Input:

if (cond) {
  return 2;
}

Output:

if (cond) return 2;

Corner case: in the example below, the curly braces around the for are needed because of the dangling else problem. If you remove the else, braces will disappear.

int dangling_else(int x)
{
  if(x<0)
    {
      for(;;)
        if(x>0)
          return 0;
    }
  else
     return 2;
}

Literal numbers

Numbers are rewritten to take less space without loss of precision. Additionally, constants that are approximately PI (with 8+ decimal digits) are are replaced with a call to acos.

Input:

0.4
0.00000012345
123456700000000
3.14159265

Output:

.4
1.2345e-7
1.234567e14
acos(-1.)

Constant arithmetic

Operations with constant arguments are evaluated, e.g. 5*2 will be replaced with 10. This is useful especially when a value has been inlined.

Conditionals evaluation

if statements, the || and && operators, as well as the ? : ternary operator are also simplified when a condition is statically known. This is useful especially after inlining.

Input:

const int debug = false;

int foo() {
  int a = 1;
  if (debug) {
    a = 2;
  }
  // ...
}

Output:

int foo() {
  int a = 1;
  // ...
}

Commutative operators

If you write x*(y*z), Shader Minifier will not remove the parentheses because it would change the orders of evaluation (this could be a problem with floating point numbers and affect the precision of the result). Instead, it will swap the operands and return y*z*x. With additive operators, we assume we can remove the parentheses.

We apply this technique in a few cases:

  • x*(y*z) becomes y*z*x
  • x+(y+z) becomes x+y+z
  • x+(y-z) becomes x+y-z
  • x-(y+z) becomes x-y-z
  • x-(y-z) becomes x-y+z

Comma operator

When all statements in a block are expression statements (e.g. function calls or assignments) or return statements, we use the comma operators to reduce the block to one statement. This allows us to remove the curly braces.

Input:

if (x) {
  y = 123;
  x++;
}

Output:

if (x) y = 123,x++;

Note: This transformation doesn't always decrease the compressed file size. Use --no-sequence to disable it and see how it performs.

Useless else after return

When a if always ends in a return, the else can be omitted entirely.

Input:

if(c)return a();else b();

Output:

if(c)return a();b();

Ternary operator

When both branches of a if+else are an expression, the if is changed into a ternary operator. Additionally, if they end with an assignment to the same variable, the variable is extracted from the ternary operator.

Input:

if (c) {
  x = f();
} else {
  a = g();
  x = 1.0;
}

Output:

x = c ? f() : a = g(), 1.0;

When both branches of a if return immediately, the if is changed into a return that uses a ternary operator.

Input:

if (y)
  return z;
return w;

Output:

return y ? z : w;

Loops

while loops are rewritten as for loops. This sometimes enable removing semi-colons or braces, either by moving the preceding statement into the initialization of the for, or by moving the last statement of the loop body into the increment part of the for.

Input:

i = 0.;
while (i < 50) {
	f(i);
	i++;
}

Output:

for(i=0.;i<50;i++)f(i);

Merge declarations

If multiple values of the same type are declared next to each other, we can merge the definitions.

Input:

float a = 1.;
float b;
float c = 2.;

Output:

float a = 1., b, c = 2.;

Note: The order of the definitions is preserved. Try to group definitions by type before running Shader Minifier.

int x;
float y;
int z;    // Not optimized! Move this line above.

The next transformation will take care of this inside function blocks.

Group declarations

If multiple variables of the same type are declared within the same block, they will be grouped.

Input:

int a = 2;
float b = 3;
int c = 4;

Input:

int a = 2,c;
float b = 3;
c = 4;

As shown in the example, the variable name will appear twice (c above) while the type will be written only once (int above). In some cases, this might make the shader slightly longer.

Enable this transformation with the flag --move-declarations.

Augmented operators

We use the augmented operators (e.g. +=) where possible.

Input:

spe=spe*spe;
x=x-.5;
a=a+(a<<3);
a=a^a>>15;

Output:

spe*=spe;
x-=.5;
a+=a<<3;
a^=a>>15;

This transformation always reduces the size of the output. However, this seems to have negligible impact on the size after compression.

Rename vector fields

To access fields of a vector, it is possible to use .rgba fields, or .xyzw, or .stpq. Shader Minifier will rename them so that it's consistent across all the shader.

Input:

return col.r + col.g + col.b;

Outut:

return col.x + col.y + col.z;

Note: Use --field-names to select which set of names to use. This can have a small effect on the compressed file size.

Automatic inlining

Shader Minifier will try to automatically inline some variables. This happens when:

  • the variable is used only once in the current block,
  • and the variable is not used in a sub-block (e.g. inside a loop),
  • and the init value is trivial (doesn't depend on a variable).

Shader Minifier will try to automatically inline some functions. This happens when:

  • the function is a single return statement,
  • the function is called in only one place,
  • the function uses its arguments at most once,
  • the function does not modify its arguments,
  • the function has no out/inout arguments,
  • the function does not use a global that would be shadowed by a local that is in scope at the call site.

Shader minifier will try to automatically inline an argument of a function call into the function body, as a declaration that may then be further inlined. This happens when:

  • the argument value is always the same for all call sites.
  • the parameter is an 'in' parameter.
  • the function is not overloaded (otherwise, removing a parameter could conflict with another overload).

If inlining causes a bug in your code, you can disable it with --no-inlining and please report a bug.

Aggressive inlining

Shader Minifier can optionally/experimentally inline even more aggressively. Along with the above cases, it will inline more variables, including the variables used many times in the code.

This is enabled with --aggressive-inlining.

Note: Inlining can lead to repetition in the shader code, which may make the shader longer, but the output may be more compression-friendly.

Explicit inlining

Shader Minifier will always inline variables and functions that start with i_. Inlining can allow the Minifier to simplify the code further.

For example, this input:

bool i_debug = false;
int i_level = 5;

int foo(int x) {
  if (i_debug) {
    x++;
  }

  return 2 * i_level * x;
}

will be simplified into:

int foo(int x)
{
  return 10*x;
}

And this input:

float i_foo(float f, float g, float x) {
  return f*x + g*x + f*g;
}

float bar(float a) {
  return i_foo(2.0, 3.0, sin(sqrt(a)));
}

will be simplified into:

float bar(float a)
{
  return 2.*sin(sqrt(a))+3.*sin(sqrt(a))+6.;
}

Note that the function inlining is very simplistic, only supporting functions which consist of a single return statement. (Basically, cases where you would use a macro, except this gives the minifier full visibility through it.)

If you want to aggressively reduce the size of your shader, try inlining more variables. Inlining can have performance implications though (if the variable stored the result of a computation), so be careful with it.

Reassignment merging

Shader Minifier can remove or merge assignment statements in some cases:

  • if a variable is declared and reassigned on the next line;
  • if a variable is reassigned on two consecutive lines;
  • if a variable is reassigned and then part of a return.

Of course, this is not always possible (the variable should be read only once; side-effects might can prevent the optimization).

For example:

float d = 100.;
d = min(d, sdfBox(...));
d = min(d, sdfBall(...));
return d;

can be simplified into:

return min(min(100., sdfBox(...)), sdfBall(...));

Variable reuse

If a local variable is no longer used and we declare a new variable of the same type, the old variable will be reused instead.

For example:

int a = 1;
// ...
f(a);
int b = 2;
// ...
f(b);

can be simplified to:

int a = 1;
// ...
f(a);
a = 2;
// ...
f(a)'

Note: This operation reduces the number of variables in the file, which usually improves the compression. When renaming is disabled, the code can be misleading, as the variable name won't match the code behavior anymore.

Vector constructors

  • Calls to vec2, vec3, and vec4 can be simplified using swizzles.
  • Remove useless constructor, when the argument is already a vec using swizzles.
  • If all arguments are equal (but not function calls), use only one argument.
  • Replace floats with ints, as it is safe inside the vec constructors.

Input:

vec4(v1.x, v1.z, v2.r, v2.t);
vec2(v1.xx);
vec2(1.2, 1.2);
vec2(1.);

Output:

vec4(v1.xz,v2.xy);
v1.xx;
vec2(1.2);
vec2(1);

Function predeclarations

Function predeclarations are removed by the parser. Then Shader Minifier reorders the functions in the file, so that functions are declared before being called.

Note: Function reordering may break if there are #if statements at the top-level.

Unused local variables

Local variables that are not referenced are removed.

Note: In theory, this can change the behavior of the code, if the initialization value calls a function that performs a side-effect.

Dead code removal

The code after a return is removed.

Note: This is optimization is disabled when there are preprocessor directives (e.g. #if) in the block.

Unused function removal

Functions that are not called are removed. Functions listed with the flag --no-renaming-list are considered as entry-point and are not removed. By default, this is the case for main and mainImage.

Note: Use --no-remove-unused to disable this transformation.

Other functions

  • distance(a, b) becomes length(a-b).
  • pow(x, 1.) becomes x.

Renaming

There are two renaming strategies:

  • If a single shader is passed on the command line, the renaming will reuse names aggressively. It will try to find the best name, based on the context where the variable is used, in order to make the code more compressible.

  • If multiple shaders are minified together, the renaming is consistent across all files. For example, all occurrences of a name (even if it's declared multiple times in different functions or files) will get renamed the same way. So if you have duplicate code in different files, that code will still compress well with this renaming strategy.

Renaming variables and functions in a compression-friendly way is difficult. Future versions of Shader Minifier may use different heuristics.