Pine-2K optimization tips

Lets collect here the optimization tips for Pine-2K. Even if that can be a bit a moving target as that can be changed between releases :wink:
Note that Pine uses a custom compiler to compile the program in run-time to the assembler. This is done every time the program is started so there is no time or free ram for most optimizations. Many things that you expect to be optimized in your C++ program are just not done by the Pine-2K compiler.

5 Likes

Calculation of literals is not optimized.

This:
sprite(x+2+3+2+3,y, rock1);

uses 4 bytes more than this:

sprite(x+10, y, rock1);

It takes even more if the literal values are greater than 8-bits.

2 Likes

No dead-code or unused variable elimination

This takes 8-bytes even if “a” was never used for anything in that function:
a = 2000;

2 Likes

This is half true. Calculation is done left-to-right, so first it does “x+2”, then it adds 3 to the result and so on.
For the optimization to kick in, either use parenthesis:
x+(2+3+2+3)
Or, better yet, use constants so your code is less magical:

const W = 2 + 3 + 2;
x + (W + 3)

Your warning about large constants is correct.

It has no way of knowing that “a” isn’t going to be used later on (since there isn’t enough RAM to store the AST), so it can’t eliminate that code.
Actually, there is some dead-code elimination:

const f = file("test.txt", 0);
if(f == null){
  // this code will de deleted if the test.txt file exists.
}
5 Likes

A few ones:

Refactor frequent grouped calls together inside a single function.

This one is very obvious, but efficient nevertheless.

Pros:

  • Liberate PROGMEM if done right - usually, the function is called at least 2 times.

Cons:

  • Burn some CPU.
  • Slightly eat more PROGMEM if not called enough.
  • You can’t have an infinite amount of parameters, so it’ll eventually fail if you’re stuffing too much things inside.

Example:

Let’s pretend you’re using a lot of flip/mirror/color/io-SCALE followed with sprite:

// flags - 0x1 for mirror, 0x2 for flip, 0x4 for scale x2 (x1 otherwise)
function spriteOpt(x, y, image, flags, c)
{
  color(c);
  mirror(flags & 0x1);
  flip(flags & 0x2);
  io("SCALE", 1 + ((flags & 0x4) >> 2));
  sprite(x, y, image);
}

And this, even if you don’t use all features. It’s trading CPU for PROGMEM.
Also, if you’re using fixed points, grouped calls are excellent for these!

Use Arrays to represent a common set of fields, such as coordinates.

Pros:

  • Less parameters in calls (which then cost less PROGMEM by calls).
  • Gives a lot of opportunity for function-refactors (like above).
  • Is somewhat a structure and allows for somewhat OOP approach.
  • Helps a lot about handling multiple instances of a game objects, such as enemies.

Cons:

  • Burns more CPU.
  • Makes the code more complicate - comment regularly to avoid becoming crazy.
  • Might not be worth the effort if you’re dealing with 1

Example:

// 0 is x, 1 is y, 2 is the bitmap, 3 is color, 4 is HP.
const playerObject = [20*256, 20*256, playerSprite, 0, 100];
const enemyObject = [200*256, 156*256, enemySprite, 96, 100];

function drawObject(gameObject)
{
  // color(gameObject.color);
  color(gameObject[3]);
  // sprite(gameObject.x, gameObject.y, gameObject.bitmap);
  sprite(gameObject[0] / 256, gameObject[1] / 256, gameObject[2]);
}

function update()
{
  ...
  // For clarity purpose I separated playerObject and enemyObject, but another tech can leverage arrays in other way and avoid those.
  checkDeath(playerObject);
  checkDeath(enemyObject);
  ...
  drawObject(playerObject);
  drawObject(enemyObject);
  ...
}

Use pointer-arithmetic when dealing with large arrays.

Pros:

  • Will liberate some CPU and PROGMEM and RAM depending on the situations.
  • Especially useful if you’re packing fields into arrays.
  • A very good pattern to make a lot of instances.

Cons:

  • Won’t always liberate PROGMEM index-based for-loop will do a similar job.
  • Arithmetic always works in term of bytes, so it is kind of confusing having to multiply by 4.
  • It’s trading a lot of good things perf-wise for a lot of complexity - comment as you drink water, that is, often.

Example:


// 0 is x, 1 is y, 2 is the bitmap, 3 is color, 4 is HP.
const objectsStart =
[
  // Player
  20*256, 20*256, playerSprite, 0, 100,
  // Enemy
  200*256, 156*256, enemySprite, 96, 100
  // List can go on...
];
const OBJECT_FIELDS_NUMBER = 5;
const OBJECT_SIZE_IN_BYTES = OBJECT_FIELDS_NUMBER * 4;
const objectsEnd = objectsStart + 2 * OBJECT_SIZE_IN_BYTES; // Alternatively: objectsStart + length(objects) * 4

function drawObject(gameObject)
{
  ... // Unchanged
}

function update()
{
  ...
  // Iterates every objects.
  for (var object = objectsStart; object != objectsEnd; object += OBJECT_SIZE_IN_BYTES)
  {
    checkDeath(object);
    drawObject(object);
  }
  ...
}

Use a script to prepare the data into a file and load it in another script.

Pros:

  • Basically frees you from initialization, which can liberate a lot of PROGMEM.
  • Allows reusing the same gameplay code with different initialization setups -think about a level.

Cons:

  • You’re going to have duplicate code for sure, especially for the constants.
  • exec will take some time to compile the receiver script.
  • If you can define your initialization data inside an array that will never change, it’s probably not worth it.
  • You can only put literals in the arrays containing the data.
    • That means, no function, no other arrays, no bitmaps, only boolean, null, integers.
    • If you need to store a reference to a bitmap and/or functions somewhere, you’ll have to indexes and provide an array with such bitmaps/functions inside the receiver script.
    • String-literals are possible but it’s trickier - you need to have them declared somewhere in the receiver script!

Example:

src.js


// 2 enemies
const level1Enemies = [2,
  10, 10,
  30, 10
];
// 5 enemies
const level2Enemies = [5,
  30, 24,
  4, 8,
  50, 20,
  100, 30,
  86, 45
];

if (random(0, 2))
  save("levels.tmp", level1Enemies);
else
  save("levels.tmp", level2Enemies);
exec("game.js");

game.js

// First int of `levels.tmp` is
const levelData = file("levels.tmp", null);
const enemiesStart = levelData + 4; // Enemies starts at +1, which is 1 int32 (4 bytes) later.
var enemiesEnd = enemiesStart + levelData[0] * 4; // The size is stored at [0]. Also, since we're using p-arithmetic in bytes, we need to *4
...

function update()
{
  ...
  for (var enemy = enemiesStart; enemy != enemiesEnd; enemy++)
  {
    // Have fun with enemies here!
    updateEnemy(enemy);
    drawEnemy(enemy);
  }
}

2 Likes

Here’s the javascript for the pre-processor:
copy code into preprocessor.js and place it in the scripts folder of pine2k (next to the backgrounds.js and respacks.js)

//!MENU-ENTRY:Run Preprocessor
//!APP-HOOK:p2k-preprocessor

let promises = [];

start();

function start(){
	log("Preprocessing p2k files");
	(dir("pine-2k").filter( path=>path.indexOf(".") == -1))
	.forEach(project=>{
		promises.push(...(dir("pine-2k/" + project)||[])
		.filter( file=>/\.p2k$/i.test(file) )
		.map( (file) => {
			fs.writeFileSync(
				`${DATA.projectPath}/pine-2k/${project}/${file.replace(/\..*$/, "")}.js`,
				read(`pine-2k/${project}/${file}`).replace(/^#include "([^"]+)"/g, (_, name) => read(`pine-2k/${project}/${name}`)));
		}));
	});
	Promise.all(promises).then(_=>{
		log("Preprocessing complete!");
	})
	.catch(ex=>{
		log(ex);
	});
	if (hookTrigger == "p2k-preprocessor") hookArgs[1]();
}

You’ll also need to modify pine2k’s project.json file if you want the script to run automatically everytime you launch your game (trust me that can come in handy).

On line 19-23 change the following:

"pipelines": {
		"Pokitto": [
			"make-img"
		]
	},

to

"pipelines": {
		"Pokitto": [
			"p2k-preprocessor",
			"make-img"
		]
	},

From there all your pine scripts that require preprocessing must use the .p2k extension instead of .js The script will detect all .p2k files run them through the pre-processor (currently just handles any #include "filename.js" statements) and output the result to a .js file.

3 Likes