GamebuinoClassic archive

Archive of the Gamebuino Classic forum (2014-2018), rebuilt from the pages kept by the Wayback Machine: only part of the topics and pages were captured. Read-only. Removal on request: hi@gamebuino.com.

Forum › Software Development

Having fun optimizing a sprite routine

Myndale Mon Aug 10, 2015 12:31 am
This is one of those cases where looking at the assembly that the compiler generates can help tremendously, even if you decide to stick with C. You can get the assembly by enabling verbose output for compile in the IDE and see where the temporary ELF is being generated, then run that through avr-objdump:

Code: Select all
C:/arduino/hardware/tools/avr/bin/avr-objdump -h -S  C:\Users\YOUR_USER_NAME_\AppData\Local\Temp\build12345678_or_something.tmp/yourapp.cpp.elf > yourapp.lst


This shows you the assembly locations of all the functions in your code, so you can load your HEX into Simbuino, place breakpoints at relevant points and time the exact number of cycles your code takes to run. To get accurate timing you need to disable interrupts while your code is running:

Code: Select all
noInterrupts();
ultraDraw3(face, x, y);
interrupts();


A quick search of the LST file shows where this is located in memory:

Code: Select all
     3be:   f8 94          cli
     3c0:   4d 2f          mov   r20, r29
     3c2:   6c 2f          mov   r22, r28
     3c4:   8a e0          ldi   r24, 0x0A   ; 10
     3c6:   91 e0          ldi   r25, 0x01   ; 1
     3c8:   0e 94 93 01    call   0x326   ; 0x326 <_Z10ultraDraw3Phcc>
     3cc:   78 94          sei


As for optimization, a few things stick out immediately:

  • the test for "if(y>=0)" is being done inside the loop, it only needs to be done at the start.
  • this loop body is only a few instructions long, so loop mechanics are going to have significant overhead. given that the loop executes exactly 8 times you'll be better off unrolling it.
  • keeping separate pointers for buf and buf2 isn't as useful on AVR as it is on other architectures due to the limited number of CPU registers that can be used for memory access. given that buf2 is always exactly 84 bytes ahead of buf you're better off using a single pointer with constant indexing.
  • arbitrary shifts on the AVRs are slooooow!

This last point is significant, look at the assembly shift statement for "*(buf2) |= data[i] >> (8-y%8);":

Code: Select all
     394:   8d 91          ld   r24, X+      ; load data[i] into r24
     396:   90 e0          ldi   r25, 0x00   ; signed-extend to 16-bits
     398:   02 2e          mov   r0, r18     ; load r0 with (8-y%8)
     39a:   02 c0          rjmp   .+4        ; skip next instruction (WHY??? pseudo-optimization perhaps?)
     39c:   95 95          asr   r25         ; shift r25:r24 right by one bit
     39e:   87 95          ror   r24
     3a0:   0a 94          dec   r0          ; we done with the loop yet
     3a2:   e2 f7          brpl   .-8        ; if not then keep going


AVRs can only shift one bit at a time, so the code has to use a loop to shift by (8-y%8). Secondly, the compiler is extending "data[i]" to 16-bits and doing the shift on that even though the top byte will always be 0. On AVRs you're much better off doing shifts with a multiplication instruction which takes only 2 cycles, and given that there are only 8 possible values we may as well store it in a look-up table:

Code: Select all
     const static byte rol_lut[] = {1<<0, 1<<1, 1<<2, 1<<3, 1<<4, 1<<5, 1<<6, 1<<7}; // store this somewhere globally
     ....
     buf[0] |= *data * rol_lut[y & 7]; // do this once per pixel


This can also be used for right shifts, although in this case the result will be 16-bit and we need the upper 8 bits, which the compiler is smart enough to implement with a byte swap:

Code: Select all
     const static unsigned ror_lut[] = {0x100>>0, 0x100>>1, 0x100>>2, 0x100>>3, 0x100>>4, 0x100>>5, 0x100>>6, 0x100>>7};
     ....
     buf[84+0] |= (data[0] * ror_lut[y & 7]) >> 8; // do this once per pixel


So put this all together and here's a new version:

Code: Select all
void ultraDraw5(register byte * data, const char x, register char y) {
    if(y>=0) {
      register uint8_t* buf = ((y&0xF8)>>1) * 21 + gb.display.getBuffer() + x;
      y &= 7;
      register uint8_t scale1 = ror_lut[y];
      register uint16_t scale2 = ror_lut[y];
      buf[0] |= data[0] * scale1;
      buf[84+0] |= (data[0] * scale2) >> 8;
      buf[1] |= data[1] * scale1;
      buf[84+1] |= (data[1] * scale2) >> 8;
      buf[2] |= data[2] * scale1;
      buf[84+2] |= (data[2] * scale2) >> 8;
      buf[3] |= data[3] * scale1;
      buf[84+3] |= (data[3] * scale2) >> 8;
      buf[4] |= data[4] * scale1;
      buf[84+4] |= (data[4] * scale2) >> 8;
      buf[5] |= data[5] * scale1;
      buf[84+5] |= (data[5] * scale2) >> 8;
      buf[6] |= data[6] * scale1;
      buf[84+6] |= (data[6] * scale2) >> 8;
      buf[7] |= data[7] * scale1;
      buf[84+7] |= (data[7] * scale2) >> 8;
    }
}


According to Simbuino the original ultraDraw3 function executes in 621 cycles. With these changes alone the function now executes in 313.
Sorunome Mon Aug 10, 2015 8:18 am
Sounds nice!
I knew unrolling would save some more but I tried to do it without unrolling :)

Wouldn't it be better for you to do data++ though, else it has to add always 1 to it, then 2 etc. always twice so that if you do data++ and then &(data)

Also in my asm version I do have enough registers for three pointers, i use X for buf, Y for buf2 and Z for data, so it all fits.

EDIT: oh, also, to those interested, I made my asm routine support full clipping, I bet it isn't that optimized now, though.
Code: Select all
void ultraDraw4(byte data[], char x, char y){
        uint8_t* buf = (((y+8)&0xF8)>>1) * 21 + x + gb.display.getBuffer();
        asm volatile(
        "mov R20,%[y]\n\t"
        "ldi R17,7\n\t"
        "add R20,R17\n\t"
        "brmi End\n\t"
        "cpi %[y],48\n\t"
        "brpl End\n\t"
       
        "inc R20\n\t"
        "ldi R16,8\n\t"
        "andi R20,7\n\t"
        "cpi R20,0\n\t"
        "breq LoopAligned\n"
        "LoopStart:\n\t"
         
          "tst %[x]\n\t"
          "brmi LoopSkip\n\t"
          "cpi %[x],84\n\t"
          "brcc LoopSkip\n\t"
         
          "ld R17,Z\n\t"
          "eor R18,R18\n\t"
          "mov R19,R20\n\t"
          "clc\n\t"
          "LoopShift:\n\t" // carry is still reset from the cpi instruction or from the dec
            "rol R17\n\t"
            "rol R18\n\t"
            "dec R19\n\t"
            "brne LoopShift\n\t"

          "tst %[y]\n\t"
          "brmi LoopSkipPart\n\t"
         
          "ld R19,X\n\t"
          "eor R19,R17\n\t"
          "st X,R19\n\t"
        "LoopSkipPart:\n\t"

          "cpi %[y],40\n\t"
          "brpl LoopSkip\n\t"
         
          "ld R19,Y\n\t"
          "eor R19,R18\n\t"
          "st Y,R19\n\t"

        "LoopSkip:\n\t"
          "eor R18,R18\n\t"
          "ldi R19,1\n\t"
          "add R26,R19\n\t" // INC DOESN'T CHANGE CARRY!
          "adc R27,R18\n\t"
          "add R28,R19\n\t"
          "adc R29,R18\n\t"
          "add R30,R19\n\t"
          "adc R31,R18\n\t"
         
          "inc %[x]\n\t"
          "dec R16\n\t"
          "brne LoopStart\n\t"
        "rjmp End\n"
        "LoopAligned:\n\t"
          "tst %[x]\n\t"
          "brmi LoopAlignSkip\n\t"
       
          "cpi %[x],84\n\t"
          "brcc LoopAlignSkip\n\t"
         
          "ld R17,Z\n\t"
          "ld R18,X\n\t"
          "eor R18,R17\n\t"
          "st X,R18\n\t"
        "LoopAlignSkip:\n\t"
          "ldi R18,1\n\t"
          "add R26,R18\n\t"
          "adc R27,R20\n\t"
          "add R30,R18\n\t"
          "adc R31,R20\n\t"
          "inc %[x]\n\t"
          "dec R16\n\t"
          "brne LoopAligned\n"
        "End:\n\t"
        ::"x" (buf - 84),"y" (buf),"z" (data),[y] "r" (y),[x] "r" (x):"r16","r17","r18","r19","r20");
}
Myndale Tue Aug 11, 2015 12:39 am
Sorunome wrote:Wouldn't it be better for you to do data++ though, else it has to add always 1 to it, then 2 etc. always twice so that if you do data++ and then &(data)


You'd think so, wouldn't you? But this is GCC/AVR, which is a poor compiler at the best of times. In practice it generates virtually identical code in both cases except for this little gem:

Code: Select all
    3be:   23 2f         mov   r18, r19         ; previous *buf is in r19, "save" it to r18
    3c0:   33 27         eor   r19, r19         ; clear r19...umm...for what purpose, exactly?
    3c2:   24 2b         or   r18, r20          ; "or" *data pixels with *buf pixels
    3c4:   28 83         st   Y, r18            ; save result back into *buf
    3c6:   12 96         adiw   r26, 0x02       ; advance data ptr
    3c8:   3c 91         ld   r19, X            ; load next *buf pixel into r19. (wtf?)


This looks very much to me like a bug in the optimizer. For some reason the compiler thinks it needs to clear r19 (which wastes 1 cycle) so it transfers its value into r18 (which wastes another). Net effect is a total of 16 wasted cycles. You'll also notice that it advances data (r26) by 2 bytes instead of 1, if you look at the code you'll see it actually modifies the value of data in your original version a total of 4 times: twice by +2, once by -2 and once more by -1 for a net total of +1 all up. And then it compensates for the ptr being out by 2 bytes by using the offset forms of LDD which are 3 cycles instead of 1. Madness!
jonnection Tue Aug 11, 2015 5:13 am
Wow. horror.

I wonder if the fact that avr has no positive accumulation in hardware has any effect

If you set *data to data[8] and then data-- (draw bytes in reverse, right to left) will the compiler just dec the pointer or do fancy dance moves again? I'd try but unfortunately no time for that :(
Sorunome Tue Aug 11, 2015 8:44 am
Myndale wrote:
Sorunome wrote:Wouldn't it be better for you to do data++ though, else it has to add always 1 to it, then 2 etc. always twice so that if you do data++ and then &(data)


You'd think so, wouldn't you? But this is GCC/AVR, which is a poor compiler at the best of times. In practice it generates virtually identical code in both cases except for this little gem:

Code: Select all
    3be:   23 2f         mov   r18, r19         ; previous *buf is in r19, "save" it to r18
    3c0:   33 27         eor   r19, r19         ; clear r19...umm...for what purpose, exactly?
    3c2:   24 2b         or   r18, r20          ; "or" *data pixels with *buf pixels
    3c4:   28 83         st   Y, r18            ; save result back into *buf
    3c6:   12 96         adiw   r26, 0x02       ; advance data ptr
    3c8:   3c 91         ld   r19, X            ; load next *buf pixel into r19. (wtf?)


This looks very much to me like a bug in the optimizer. For some reason the compiler thinks it needs to clear r19 (which wastes 1 cycle) so it transfers its value into r18 (which wastes another). Net effect is a total of 16 wasted cycles. You'll also notice that it advances data (r26) by 2 bytes instead of 1, if you look at the code you'll see it actually modifies the value of data in your original version a total of 4 times: twice by +2, once by -2 and once more by -1 for a net total of +1 all up. And then it compensates for the ptr being out by 2 bytes by using the offset forms of LDD which are 3 cycles instead of 1. Madness!

The compiler could have just used st Y+,r18 that way Y would increase in one cycle..... :P
deeph Wed Dec 23, 2015 4:22 pm
Is there an easy way to use it like that too :

Code: Select all
const byte tiles[]={
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x04,0x20,0x00,0x00,0x08,0x00,
  0x40,0x82,0x14,0x20,0x00,0x11,0x22,0x00,
  0x00,0x00,0x00,0x00,0x20,0x12,0x54,0x00,
  0x00,0x00,0x00,0x00,0x10,0x12,0x54,0x00,
  0xff,0xfd,0xdf,0xaf,0xfb,0xf5,0xff,0xdf,
  0xfd,0xdf,0xaf,0xfb,0xf5,0xff,0xdf,0xff,
  0x18,0x24,0x4a,0x67,0xdb,0xbf,0xdf,0x7e,
  0x7e,0x81,0xa9,0xb5,0x81,0x7e,0x18,0x18,
  0x03,0x1c,0x28,0x20,0x54,0x48,0x60,0x80,
  0xc1,0xb0,0x6a,0x57,0x3b,0x1e,0x04,0x03,
  0xc0,0x38,0x14,0x04,0x0a,0xa2,0x16,0xab,
  0x55,0x29,0x56,0xbe,0xec,0x78,0x20,0xc0 };

  [...]
  ultraDraw4(tiles[tile_num*TILE_HEIGHT], x, y);
  [...]

I'm trying to speed up my tilemapper, and as I don't know avr asm this could really be useful :)
Sorunome Mon Dec 28, 2015 3:08 am
No, sorry, this sprite routine is only working for 8x8 sprites, everything else would require a whole different routine, this routine is optimized ONLY for drawing 8x8 sprites ;)


Or what do you mean with "something like that"?
deeph Mon Dec 28, 2015 8:35 am
The sprites (tiles) are 8*8 but I'd like to give an array with an offset as a parameter of the routine, instead of a pointer. And by the way, I think it should be easy to allow the use of a custom height, no ?
Sorunome Mon Dec 28, 2015 2:23 pm
Ironically a custom width would be easy, the way the screen is built.
As per multiple 8x8 prites, you should be able do do something like pointer+8 to get the next sprite.
deeph Sat Jan 02, 2016 12:36 pm
I get several errors while trying to compile :

Assembler messages:
Error: register number above 15 required
Error: symbol `LoopStart' is already defined
Error: register number above 15 required
Error: symbol `LoopShift' is already defined
Error: symbol `LoopSkipPart' is already defined
Error: register number above 15 required
Error: symbol `LoopSkip' is already defined
Error: symbol `LoopAligned' is already defined
Error: register number above 15 required
Error: symbol `LoopAlignSkip' is already defined
Error: symbol `End' is already defined
Error: register number above 15 required
Error: symbol `LoopStart' is already defined
Error: register number above 15 required
Error: symbol `LoopShift' is already defined
Error: symbol `LoopSkipPart' is already defined
Error: register number above 15 required
Error: symbol `LoopSkip' is already defined
Error: symbol `LoopAligned' is already defined
Error: register number above 15 required
Error: symbol `LoopAlignSkip' is already defined
Error: symbol `End' is already defined


It seems that some instructions are not understood ?

edit : I solved the labels errors using this : http://forum.arduino.cc/index.php?topic=51165.0 , so now I have this :

Code: Select all
void draw_sprite(const byte data[], char x, char y){  // routine by Sorunome
  uint8_t* buf = (((y+8)&0xF8)>>1) * 21 + x + gb.display.getBuffer();
  asm volatile(
  "mov R20,%[y]\n\t"
  "ldi R17,7\n\t"
  "add R20,R17\n\t"
  "brmi 6f\n\t"
  "cpi %[y],48\n\t"
  "brpl 6f\n\t"
 
  "inc R20\n\t"
  "ldi R16,8\n\t"
  "andi R20,7\n\t"
  "cpi R20,0\n\t"
  "breq 4f\n"
  "0:\n\t"
   
   "tst %[x]\n\t"
   "brmi 3f\n\t"
   "cpi %[x],84\n\t"
   "brcc 3f\n\t"
   
   "ld R17,Z\n\t"
   "eor R18,R18\n\t"
   "mov R19,R20\n\t"
   "clc\n\t"
   "1:\n\t" // carry is still reset from the cpi instruction or from the dec
   "rol R17\n\t"
   "rol R18\n\t"
   "dec R19\n\t"
   "brne 1b\n\t"

   "tst %[y]\n\t"
   "brmi 2f\n\t"
   
   "ld R19,X\n\t"
   "or R19,R17\n\t"
   "st X,R19\n\t"
  "2:\n\t"

   "cpi %[y],48\n\t"
   "brpl 3f\n\t"
   
   "ld R19,Y\n\t"
   "or R19,R18\n\t"
   "st Y,R19\n\t"

  "3:\n\t"
   "eor R18,R18\n\t"
   "ldi R19,1\n\t"
   "add R26,R19\n\t" // INC DOESN'T CHANGE CARRY!
   "adc R27,R18\n\t"
   "add R28,R19\n\t"
   "adc R29,R18\n\t"
   "add R30,R19\n\t"
   "adc R31,R18\n\t"
   
   "inc %[x]\n\t"
   "dec R16\n\t"
   "brne 0b\n\t"
  "rjmp 6f\n"
  "4:\n\t"
   "tst %[x]\n\t"
   "brmi 5f\n\t"
 
   "cpi %[x],84\n\t"
   "brcc 5f\n\t"
   
   "ld R17,Z\n\t"
   "ld R18,X\n\t"
   "eor R18,R17\n\t"
   "st X,R18\n\t"
  "5:\n\t"
   "ldi R18,1\n\t"
   "add R26,R18\n\t"
   "adc R27,R20\n\t"
   "add R30,R18\n\t"
   "adc R31,R20\n\t"
   "inc %[x]\n\t"
   "dec R16\n\t"
   "brne 4b\n"
  "6:\n\t"
  ::"x" (buf - 84),"y" (buf),"z" (data),[y] "r" (y),[x] "r" (x):"r16","r17","r18","r19","r20");
}


But I still get the register numbers errors... I think that the parameters x & y are put in a register numbered under 15, so is there a way to bypass this, knowing that every other register seems to be already used in the routine ?
Sorunome Sat Jan 09, 2016 4:40 pm
[x] and [y] etc. are set by the compiler itself, this seems rather odd to me. Are you using the arduino IDE and trying to compile as gamebuino? Which version of the arduino IDE?
deeph Sat Jan 09, 2016 5:12 pm
Yes, I'm using arduino IDE unhanced release 1.0.5 and compiling as gamebuino. Don't know if there's a newer version, though.

edit : nevermind, using version 1.6.7 it seems to compile.
Sorunome Sun Jan 10, 2016 8:24 pm
Glad you got it working, and yeah, I remember it only working in 1.6.x + or something.
deeph Sun Mar 13, 2016 1:26 pm
Ok I get it to work as I wanted, but is there a way to XOR the sprite instead of OR it ? Thanks.
Sorunome Sun Mar 13, 2016 5:09 pm
Try this slight modification (untested)
Code: Select all
void ultraDraw4(byte data[], char x, char y){
        uint8_t* buf = ((y&0xF8)>>1) * 21 + gb.display.getBuffer() + x;
        asm volatile(
        "ldi R16,8\n\t"
        "cpi %[rotnumber],0\n\t"
        "breq LoopAligned\n"
        "LoopStart:\n\t"

          "ld R17,Z+\n\t"
          "eor R18,R18\n\t"
          "mov R19,%[rotnumber]\n\t"
          "LoopShift:\n\t" // carry is still reset from the cpi instruction or from the dec
            "rol R17\n\t"
            "rol R18\n\t"
            "dec R19\n\t"
            "brne LoopShift\n\t"

          "ld R19,X\n\t"
          "eor R19,R17\n\t"
          "st X+,R19\n\t"
         
          "ld R19,Y\n\t"
          "eor R19,R18\n\t"
          "st Y+,R19\n\t"
         
          "dec R16\n\t"
          "brne LoopStart\n\t"
        "rjmp End\n"
        "LoopAligned:\n\t"
          "ld R17,Z+\n\t"
          "ld R18,X\n\t"
          "eor R18,R17\n\t"
          "st X+,R18\n\t"
          "dec R16\n\t"
          "brne LoopAligned\n"
        "End:\n"
        ::"x" (buf),"y" (buf + 84),"z" (data),[rotnumber] "r" (y%8):"r16","r17","r18","r19");
}
deeph Sun Mar 13, 2016 6:21 pm
It's working but I need to be able to switch the copying logic (and/or/xor), mainly to mask sprites. Is there a way to do self modifying code, or adding an extra parameter to the routine ? Thank you !! :)
Sorunome Mon Mar 14, 2016 12:19 pm
Not easily within one routine.
You can make multiple, however
you see there are three key point lines:
"eor R19,R17\n\t"

"eor R19,R18\n\t"

"eor R18,R17\n\t"


eor is for xor, or is for or, and is for and
deeph Mon Mar 14, 2016 5:57 pm
Mmh ok, I think I'm going to do the mask with the classic sprite routine and everything else with the asm one. Thank you :)
Sorunome Tue Mar 15, 2016 11:04 pm
hmm, you could actually do some more dark magic optimizing with a clipped routine directly in the asm code, i currently don't have my arduino dev enviroment set up, though :(
deeph Wed Mar 16, 2016 6:00 am
Yes, I'm using yours : http://gamebuino.com/forum/viewtopic.php?f=12&t=3244&start=10#p10176

Here's a new version of my tilemapper using it for drawing (minus sprites masking) :

Image

Code: Select all
#include <SPI.h>
#include <Gamebuino.h>

#define MAP_WIDTH 15
#define MAP_HEIGHT 15
const int PROGMEM map_test[]={
  0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,
  0x05,0x01,0x01,0x01,0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,0x05,
  0x05,0x01,0x00,0x00,0x01,0x07,0x01,0x01,0x01,0x01,0x05,0x05,0x05,0x05,0x05,
  0x05,0x01,0x00,0x00,0x00,0x00,0x07,0x07,0x07,0x00,0x01,0x05,0x05,0x05,0x05,
  0x05,0x01,0x00,0x03,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x01,0x05,0x05,0x05,
  0x05,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x05,0x05,
  0x05,0x01,0x00,0x01,0x01,0x02,0x02,0x02,0x01,0x01,0x00,0x00,0x01,0x05,0x05,
  0x05,0x05,0x01,0x01,0x02,0x02,0x02,0x02,0x02,0x01,0x09,0x0b,0x01,0x05,0x05,
  0x05,0x01,0x00,0x01,0x01,0x02,0x02,0x02,0x01,0x01,0x0a,0x0c,0x01,0x05,0x05,
  0x05,0x01,0x00,0x09,0x0b,0x01,0x02,0x02,0x01,0x00,0x00,0x00,0x01,0x05,0x05,
  0x05,0x05,0x01,0x0a,0x0c,0x00,0x00,0x02,0x00,0x00,0x00,0x01,0x05,0x05,0x05,
  0x05,0x05,0x01,0x01,0x00,0x00,0x03,0x02,0x08,0x01,0x01,0x01,0x05,0x05,0x05,
  0x05,0x05,0x05,0x05,0x01,0x01,0x00,0x02,0x01,0x05,0x05,0x05,0x05,0x05,0x05,
  0x05,0x05,0x05,0x05,0x05,0x05,0x01,0x02,0x05,0x05,0x05,0x05,0x05,0x05,0x05,
  0x05,0x05,0x05,0x05,0x05,0x05,0x02,0x01,0x05,0x05,0x05,0x05,0x05,0x05,0x05 };

#define TILE_WIDTH 8
#define TILE_HEIGHT 8
#define TILES_PASSABLE_6End 4
#define TILES_ANIMATED_START 3
#define TILES_ANIMATED_6End 5
#define ANIMATION_FREQUENCY 500 // ms

const byte tiles[]={
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x08,0x00,0x40,0x04,0x00,0x00,
  0x02,0x01,0x48,0x24,0x00,0x04,0x42,0x20,
  0x00,0x40,0x10,0x60,0x00,0x40,0x20,0x00,
  0x00,0x40,0x00,0x70,0x00,0x40,0x20,0x00,
  0xff,0xf7,0x7b,0xf7,0xdf,0xef,0xdd,0xff,
  0xff,0xfb,0xbd,0xfb,0xef,0xf7,0xee,0xff,
  0x70,0xdc,0xaa,0xf1,0xf5,0xea,0xfc,0x78,
  0x1e,0x21,0x2d,0xe9,0xe5,0x29,0x21,0x1e,
  0x80,0x70,0x4c,0x12,0x26,0x12,0x01,0x01,
  0x03,0x0d,0x16,0x3a,0x34,0x68,0xbc,0x99,
  0xa1,0x01,0xa2,0x46,0x92,0x4c,0xf0,0x80,
  0x98,0xb5,0x7a,0x2d,0x3a,0x1d,0x0c,0x03,
 };
const byte* tiles_pointer = tiles;

const byte player_charset[]={
  0x00,0x1e,0xed,0xa5,0xed,0xa5,0x1e,0x04,
  0x00,0xde,0xed,0xe5,0xad,0xe5,0x9e,0x04,
  0x00,0xde,0xad,0xe5,0xed,0xe5,0x9e,0x04,
  0x00,0x5e,0xa5,0xe5,0x65,0xe5,0xa5,0x5e,
  0x00,0x7e,0xd5,0xe5,0x65,0xa5,0xa5,0x7e,
  0x00,0x7e,0xa5,0xa5,0x65,0xe5,0xd5,0x7e,
  0x00,0x04,0x1e,0xa5,0xed,0xa5,0xed,0x1e,
  0x00,0x04,0x9e,0xe5,0xad,0xe5,0xed,0xde,
  0x00,0x04,0x9e,0xe5,0xed,0xe5,0xad,0xde,
  0x00,0x5e,0xa5,0xed,0x65,0xed,0xa5,0x5e,
  0x00,0x7e,0xd5,0xed,0x65,0xed,0xe5,0x7e,
  0x00,0x7e,0xe5,0xed,0x65,0xed,0xd5,0x7e,
 };
const byte* player_charset_pointer = player_charset;

const byte PROGMEM player_mask01[]={
  8,8,0x3c,0x7e,0x7f,0x7e,0x7e,0x3c,0x38,0x3c };
const byte PROGMEM player_mask02[]={
  8,8,0x3c,0x7e,0x7f,0x7e,0x7e,0x3c,0x7c,0x7e };
const byte PROGMEM player_mask03[]={
  8,8,0x3e,0x7f,0x7f,0x7f,0x7f,0x3e,0x7f,0x36 };
const byte PROGMEM player_mask04[]={
  8,8,0x3e,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x36 };
const byte PROGMEM player_mask05[]={
  8,8,0x1e,0x3f,0x7f,0x3f,0x3f,0x1e,0x0e,0x1e };
const byte PROGMEM player_mask06[]={
  8,8,0x1e,0x3f,0x7f,0x3f,0x3f,0x1e,0x1f,0x3f };
const byte* player_masks_pointer[]={
  player_mask01,player_mask02,player_mask02,player_mask03,player_mask04,player_mask04,player_mask05,player_mask06,player_mask06,player_mask03,player_mask04,player_mask04 };

Gamebuino gb;

void setup(){
  gb.begin();
  byte player_x = 2;
  byte player_y = 2;
  byte player_direction = 3;
  byte player_animation = 0;
  int camera_x = player_x*TILE_WIDTH-LCDWIDTH/2+4;
  camera_x = camera_x*(camera_x > 0)+(MAP_WIDTH*TILE_WIDTH-LCDWIDTH-camera_x)*(camera_x > MAP_WIDTH*TILE_WIDTH-LCDWIDTH);
  int camera_y = player_y*TILE_HEIGHT-LCDHEIGHT/2+4;
  camera_y = camera_y*(camera_y > 0)+(MAP_HEIGHT*TILE_HEIGHT-LCDHEIGHT-camera_y)*(camera_y > MAP_HEIGHT*TILE_HEIGHT-LCDHEIGHT);
  while(1){
    if(gb.update()){
      draw_map(camera_x, camera_y);
      draw_player(player_x*TILE_WIDTH-camera_x, player_y*TILE_WIDTH-camera_y, player_direction, player_animation);
      char x_temp = -gb.buttons.repeat(BTN_LEFT, 1)*(player_x > 0)+gb.buttons.repeat(BTN_RIGHT, 1)*(player_x < MAP_WIDTH-1);
      char y_temp = -gb.buttons.repeat(BTN_UP, 1)*(player_y > 0)+gb.buttons.repeat(BTN_DOWN, 1)*(player_y < MAP_HEIGHT-1);
      if(x_temp || y_temp){
        player_direction = (1+x_temp)*(x_temp != 0);
        player_direction = (2+y_temp)*(y_temp != 0 || player_direction == 0);
        if(TILES_PASSABLE_6End-pgm_read_byte(map_test+(player_y+y_temp)*MAP_WIDTH+player_x+x_temp) > 0){
          for(byte i = 1; i <=8; i++){
            player_animation += 1;
            player_animation *= (player_animation < 3 && i < 8);
            camera_x = (player_x*TILE_WIDTH-LCDWIDTH/2+4+i*x_temp);
            camera_x = camera_x*(camera_x > 0)+(MAP_WIDTH*TILE_WIDTH-LCDWIDTH-camera_x)*(camera_x > MAP_WIDTH*TILE_WIDTH-LCDWIDTH);
            camera_y = player_y*TILE_HEIGHT-LCDHEIGHT/2+4+i*y_temp;
            camera_y = camera_y*(camera_y > 0)+(MAP_HEIGHT*TILE_HEIGHT-LCDHEIGHT-camera_y)*(camera_y > MAP_HEIGHT*TILE_HEIGHT-LCDHEIGHT);
            gb.display.clear();
            draw_map(camera_x, camera_y);
            draw_player(player_x*TILE_WIDTH-camera_x+i*x_temp, player_y*TILE_WIDTH-camera_y+i*y_temp, player_direction, player_animation);
            gb.display.update();
          }
          player_x += x_temp;
          player_y += y_temp;
        }
      }
    }
  }
}

void loop(){
}

void draw_map(int camera_x, int camera_y){
  for(byte y = 0; y <= 6; y++){
    for(byte x = 0; x <= 11; x++){
      int tile_x = camera_x/TILE_WIDTH+x;
      int tile_y = camera_y/TILE_HEIGHT+y;
      if(tile_x >= 0 && tile_x < MAP_WIDTH && tile_y >= 0 && tile_y < MAP_HEIGHT){
        byte tile_num = pgm_read_byte(map_test+tile_y*MAP_WIDTH+tile_x);
        tile_num += (tile_num >= TILES_ANIMATED_START && tile_num <= TILES_ANIMATED_6End)*millis()/ANIMATION_FREQUENCY%2;
        draw_sprite(tiles_pointer+tile_num*TILE_HEIGHT, x*TILE_WIDTH-camera_x%TILE_WIDTH, y*TILE_HEIGHT-camera_y%TILE_HEIGHT);
      }
    }
  }
}

void draw_player(int x, int y, byte direction, byte animation){
  gb.display.setColor(WHITE, BLACK);
  gb.display.drawBitmap(x, y, player_masks_pointer[direction*3+animation]);
  draw_sprite(player_charset_pointer+(direction*24+animation*8), x, y);
}


void draw_sprite(const byte data[], char x, char y){  // routine by Sorunome
  uint8_t* buf = (((y+8)&0xF8)>>1) * 21 + x + gb.display.getBuffer();
  asm volatile(
  "mov R20,%[y]\n\t"
  "ldi R17,7\n\t"
  "add R20,R17\n\t"
  "brmi End\n\t"
  "cpi %[y],48\n\t"
  "brpl End\n\t"
  "inc R20\n\t"
  "ldi R16,8\n\t"
  "andi R20,7\n\t"
  "cpi R20,0\n\t"
  "breq LoopAligned\n"
  "LoopStart:\n\t"
  "tst %[x]\n\t"
  "brmi LoopSkip\n\t"
  "cpi %[x],84\n\t"
  "brcc LoopSkip\n\t"
  "ld R17,Z\n\t"
  "eor R18,R18\n\t"
  "mov R19,R20\n\t"
  "clc\n\t"
  "LoopShift:\n\t" // carry is still reset from the cpi instruction or from the dec
  "rol R17\n\t"
  "rol R18\n\t"
  "dec R19\n\t"
  "brne LoopShift\n\t"
  "tst %[y]\n\t"
  "brmi LoopSkipPart\n\t"
  "ld R19,X\n\t"
  "eor R19,R17\n\t"
  "st X,R19\n\t"
  "LoopSkipPart:\n\t"
  "cpi %[y],40\n\t"
  "brpl LoopSkip\n\t"
  "ld R19,Y\n\t"
  "eor R19,R18\n\t"
  "st Y,R19\n\t"
  "LoopSkip:\n\t"
  "eor R18,R18\n\t"
  "ldi R19,1\n\t"
  "add R26,R19\n\t" // INC DOESN'T CHANGE CARRY!
  "adc R27,R18\n\t"
  "add R28,R19\n\t"
  "adc R29,R18\n\t"
  "add R30,R19\n\t"
  "adc R31,R18\n\t"
  "inc %[x]\n\t"
  "dec R16\n\t"
  "brne LoopStart\n\t"
  "rjmp End\n"
  "LoopAligned:\n\t"
  "tst %[x]\n\t"
  "brmi LoopAlignSkip\n\t"
  "cpi %[x],84\n\t"
  "brcc LoopAlignSkip\n\t"
  "ld R17,Z\n\t"
  "ld R18,X\n\t"
  "eor R18,R17\n\t"
  "st X,R18\n\t"
  "LoopAlignSkip:\n\t"
  "ldi R18,1\n\t"
  "add R26,R18\n\t"
  "adc R27,R20\n\t"
  "add R30,R18\n\t"
  "adc R31,R20\n\t"
  "inc %[x]\n\t"
  "dec R16\n\t"
  "brne LoopAligned\n"
  "End:\n\t"
  ::"x" (buf - 84),"y" (buf),"z" (data),[y] "r" (y),[x] "r" (x):"r16","r17","r18","r19","r20");
}


I don't know how much times faster/smaller it is, but it is.

I'll probably make a little game out of this :)

Again, thanks !