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
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.
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.
No dead-code or unused variable elimination
This takes 8-bytes even if âaâ was never used for anything in that function:
a = 2000;
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.
}
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.
- @tuxinator2009 circumvented it by creating a preprocessor system.
-
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);
}
}
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.