Skip to content

Commit

Permalink
Bangle.js: Added fastpaths for 2 and 4 bit arraybuffers, and massivel…
Browse files Browse the repository at this point in the history
…y improve 1 bit fills
  • Loading branch information
gfwilliams committed Oct 9, 2024
1 parent ec1ec87 commit ad0f468
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 21 deletions.
2 changes: 2 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
Waveform: Add 'npin' option to allow +/- output on two pins
Waveform: Add ability to play directly from Storage
Puck.js: Remove networking support from default build, add PUCKJS_NETWORK -> espruino_2vxx_puckjs_network.zip builds for those that need it
Bangle.js: Added fastpaths for 2 and 4 bit arraybuffers, and massively improve 1 bit fills
Graphic.createArrayBuffer msb now defaults to true as it's rare to ever need msb:false

2v24 : Bangle.js2: Add 'Bangle.touchRd()', 'Bangle.touchWr()'
Bangle.js2: After Bangle.showTestScreen, put Bangle.js into a hard off state (not soft off)
Expand Down
11 changes: 7 additions & 4 deletions libs/graphics/jswrap_graphics.c
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ static bool isValidBPP(int bpp) {
"An object of other options. `{ zigzag : true/false(default), vertical_byte : true/false(default), msb : true/false(default), color_order: 'rgb'(default),'bgr',etc }`",
"`zigzag` = whether to alternate the direction of scanlines for rows",
"`vertical_byte` = whether to align bits in a byte vertically or not",
"`msb` = when bits<8, store pixels most significant bit first, when bits>8, store most significant byte first",
"`msb` = when bits<8, store pixels most significant bit first, when bits>8, store most significant byte first (as of 2v25, msb:true is default)",
"`interleavex` = Pixels 0,2,4,etc are from the top half of the image, 1,3,5,etc from the bottom half. Used for P3 LED panels.",
"`color_order` = re-orders the colour values that are supplied via setColor",
"`buffer` = if specified, createArrayBuffer won't create a new buffer but will use the given one"
Expand Down Expand Up @@ -630,15 +630,18 @@ JsVar *jswrap_graphics_createArrayBuffer(int width, int height, int bpp, JsVar *
JsGraphics gfx;
gfx.data.type = JSGRAPHICSTYPE_ARRAYBUFFER;
graphicsStructInit(&gfx,width,height,bpp);
gfx.data.flags = JSGRAPHICSFLAGS_NONE;
gfx.data.flags = JSGRAPHICSFLAGS_ARRAYBUFFER_MSB;
gfx.graphicsVar = parent;

JsVar *optionalBuffer = 0;
if (jsvIsObject(options)) {
JsVar *v;
if (jsvObjectGetBoolChild(options, "zigzag"))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_ARRAYBUFFER_ZIGZAG);
if (jsvObjectGetBoolChild(options, "msb"))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_ARRAYBUFFER_MSB);
if ((v = jsvObjectGetChildIfExists(options, "msb"))) {
if (!jsvGetBoolAndUnLock(v))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags & ~JSGRAPHICSFLAGS_ARRAYBUFFER_MSB);
}
if (jsvObjectGetBoolChild(options, "interleavex"))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_ARRAYBUFFER_INTERLEAVEX);
if (jsvObjectGetBoolChild(options, "vertical_byte")) {
Expand Down
155 changes: 144 additions & 11 deletions libs/graphics/lcd_arraybuffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "lcd_arraybuffer.h"
#include "jsvar.h"
#include "jsvariterator.h"
#include "jsinteractive.h"

#ifndef SAVE_ON_FLASH
#ifndef ESPRUINOBOARD
Expand Down Expand Up @@ -261,31 +262,149 @@ void lcdScroll_ArrayBuffer_flat(JsGraphics *gfx, int xdir, int ydir, int x1, int
}

#ifdef GRAPHICS_FAST_PATHS
// 1 bit
void lcdSetPixel_ArrayBuffer_flat1(JsGraphics *gfx, int x, int y, unsigned int col) {
int p = x + y*gfx->data.width;
if (col) ((uint8_t*)gfx->backendData)[p>>3] |= (uint8_t)(0x80 >> (p&7));
else ((uint8_t*)gfx->backendData)[p>>3] &= (uint8_t)(0xFF7F >> (p&7));
uint8_t *byte = &((uint8_t*)gfx->backendData)[p>>3];
if (col) *byte |= (uint8_t)(0x80 >> (p&7));
else *byte &= (uint8_t)(0xFF7F >> (p&7));
}
unsigned int lcdGetPixel_ArrayBuffer_flat1(struct JsGraphics *gfx, int x, int y) {
int p = x + y*gfx->data.width;
uint8_t byte = ((uint8_t*)gfx->backendData)[p>>3];
return (byte >> (7-(p&7))) & 1;
}

void lcdFillRect_ArrayBuffer_flat1(JsGraphics *gfx, int x1, int y1, int x2, int y2, unsigned int col) {
if (x2-x1 < 8) return lcdFillRect_ArrayBuffer_flat(gfx,x1,y2,x2,y2,col); // not worth trying to work around this
uint8_t *pixels = (uint8_t *)gfx->backendData;
// build up 8 pixels in 1 byte for fast writes
col &= 1;
uint8_t colByte = (uint8_t)col | (uint8_t)(col<<1);
colByte |= colByte<<2;
colByte |= colByte<<4;
// do each row separately
for (int y=y1;y<=y2;y++) {
int py = y*gfx->data.width;
int p = x1 + py; // pixel index (not bit)
int p2 = x2 + py; // pixel index (not bit)
uint8_t *byte = &pixels[p>>2];
// start off unaligned
if (p&7) {
int amt = (8-(p&3));
int mask = ~(0xFF << amt);
*byte = (*byte & ~mask) | (colByte & mask);
byte++;
p = (p&~7)+8;
}
// now we're aligned, just write bytes
while (p+7<=p2) {
*(byte++) = colByte;
p+=8;
}
// finish off unaligned
if (p<=p2) {
int amt = 1+p2-p;
int mask = ~(0xFF >> amt);
*byte = (*byte & ~mask) | (colByte & mask);
}
}
}
// 2 bit
void lcdSetPixel_ArrayBuffer_flat2(JsGraphics *gfx, int x, int y, unsigned int col) {
int p = (x + y*gfx->data.width); // pixel index (not bit)
int b = (p&3) << 1; // bit
uint8_t *byte = &((uint8_t*)gfx->backendData)[p>>2];
*byte = (*byte & (0xFF3F>>b)) | ((col&3)<<(6-b));
}
unsigned int lcdGetPixel_ArrayBuffer_flat2(struct JsGraphics *gfx, int x, int y) {
int p = x + y*gfx->data.width; // pixel index (not bit)
int b = (p&3) << 1; // bit
uint8_t *byte = &((uint8_t*)gfx->backendData)[p>>2];
return (*byte >> (6-b)) & 3;
}
void lcdFillRect_ArrayBuffer_flat2(JsGraphics *gfx, int x1, int y1, int x2, int y2, unsigned int col) {
if (x2-x1 < 4) return lcdFillRect_ArrayBuffer_flat(gfx,x1,y2,x2,y2,col); // not worth trying to work around this
uint8_t *pixels = (uint8_t *)gfx->backendData;
// build up 4 pixels in 1 byte for fast writes
col &= 3;
uint8_t colByte = (uint8_t)col | (uint8_t)(col<<2);
colByte |= colByte<<4;
// do each row separately
for (int y=y1;y<=y2;y++) {
int p = x1 + y*gfx->data.width;
for (int x=x1;x<=x2;x++) {
if (col) ((uint8_t*)gfx->backendData)[p>>3] |= (uint8_t)(0x80 >> (p&7));
else ((uint8_t*)gfx->backendData)[p>>3] &= (uint8_t)(0xFF7F >> (p&7));
int py = y*gfx->data.width;
int p = x1 + py; // pixel index (not bit)
int p2 = x2 + py; // pixel index (not bit)
uint8_t *byte = &pixels[p>>2];
// start off unaligned
if (p&3) {
int amt = (4-(p&3));
int mask = ~(0xFF << (amt<<1));
*byte = (*byte & ~mask) | (colByte & mask);
byte++;
p = (p&~3)+4;
}
// now we're aligned, just write bytes
while (p+3<=p2) {
*(byte++) = colByte;
p+=4;
}
// finish off unaligned
if (p<=p2) {
int amt = 1+p2-p;
int mask = ~(0xFF >> (amt<<1));
*byte = (*byte & ~mask) | (colByte & mask);
}
}
}
// 4 bit
void lcdSetPixel_ArrayBuffer_flat4(JsGraphics *gfx, int x, int y, unsigned int col) {
int p = (x + y*gfx->data.width); // pixel index (not bit)
int b = (p&1) << 2; // bit
uint8_t *byte = &((uint8_t*)gfx->backendData)[p>>1];
*byte = (*byte & (0xFF0F>>b)) | ((col&15)<<(4-b));
}
unsigned int lcdGetPixel_ArrayBuffer_flat4(struct JsGraphics *gfx, int x, int y) {
int p = x + y*gfx->data.width; // pixel index (not bit)
int b = (p&1) << 2; // bit
uint8_t *byte = &((uint8_t*)gfx->backendData)[p>>1];
return (*byte >> (4-b)) & 15;
}
void lcdFillRect_ArrayBuffer_flat4(JsGraphics *gfx, int x1, int y1, int x2, int y2, unsigned int col) {
if (x2-x1 < 2) return lcdFillRect_ArrayBuffer_flat(gfx,x1,y2,x2,y2,col); // not worth trying to work around this
uint8_t *pixels = (uint8_t *)gfx->backendData;
// build up 4 pixels in 1 byte for fast writes
col &= 15;
uint8_t colByte = (uint8_t)col | (uint8_t)(col<<4);
// do each row separately
for (int y=y1;y<=y2;y++) {
int py = y*gfx->data.width;
int p = x1 + py; // pixel index (not bit)
int p2 = x2 + py; // pixel index (not bit)
uint8_t *byte = &pixels[p>>1];
// start off unaligned
if (p&1) {
*byte = (*byte & 0xF0) | col;
byte++;
p++;
}
// now we're aligned, just write bytes
while (p+1<=p2) {
*(byte++) = colByte;
p+=2;
}
// finish off unaligned
if (p<=p2) {
*byte = (*byte & 0x0F) | (col<<4);
}
}
}

// 8 bit
void lcdSetPixel_ArrayBuffer_flat8(JsGraphics *gfx, int x, int y, unsigned int col) {
((uint8_t*)gfx->backendData)[x + y*gfx->data.width] = (uint8_t)col;
}

unsigned int lcdGetPixel_ArrayBuffer_flat8(struct JsGraphics *gfx, int x, int y) {
return ((uint8_t*)gfx->backendData)[x + y*gfx->data.width];
}

void lcdFillRect_ArrayBuffer_flat8(JsGraphics *gfx, int x1, int y1, int x2, int y2, unsigned int col) {
for (int y=y1;y<=y2;y++) {
uint8_t *p = &((uint8_t*)gfx->backendData)[x1 + y*gfx->data.width];
Expand Down Expand Up @@ -341,8 +460,22 @@ void lcdSetCallbacks_ArrayBuffer(JsGraphics *gfx) {
!(gfx->data.flags & JSGRAPHICSFLAGS_NONLINEAR)
) { // super fast path for 1 bit
gfx->setPixel = lcdSetPixel_ArrayBuffer_flat1;
gfx->getPixel = lcdGetPixel_ArrayBuffer_flat;
gfx->getPixel = lcdGetPixel_ArrayBuffer_flat1;
gfx->fillRect = lcdFillRect_ArrayBuffer_flat1;
} else if (gfx->data.bpp==2 &&
(gfx->data.flags & JSGRAPHICSFLAGS_ARRAYBUFFER_MSB) &&
!(gfx->data.flags & JSGRAPHICSFLAGS_NONLINEAR)
) { // super fast path for 1 bit
gfx->setPixel = lcdSetPixel_ArrayBuffer_flat2;
gfx->getPixel = lcdGetPixel_ArrayBuffer_flat2;
gfx->fillRect = lcdFillRect_ArrayBuffer_flat2;
} else if (gfx->data.bpp==4 &&
(gfx->data.flags & JSGRAPHICSFLAGS_ARRAYBUFFER_MSB) &&
!(gfx->data.flags & JSGRAPHICSFLAGS_NONLINEAR)
) { // super fast path for 1 bit
gfx->setPixel = lcdSetPixel_ArrayBuffer_flat4;
gfx->getPixel = lcdGetPixel_ArrayBuffer_flat4;
gfx->fillRect = lcdFillRect_ArrayBuffer_flat4;
} else if (gfx->data.bpp==8 &&
!(gfx->data.flags & JSGRAPHICSFLAGS_NONLINEAR)
) { // super fast path for 8 bits
Expand Down
2 changes: 1 addition & 1 deletion tests/test_graphics_arraybuffer_001.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
var g;
g = Graphics.createArrayBuffer(8,8,1);
g = Graphics.createArrayBuffer(8,8,1,{msb:false});
g.drawLine(0,0,8,8);
print(g.buffer);
result = g.buffer == "1,2,4,8,16,32,64,128";
Expand Down
2 changes: 1 addition & 1 deletion tests/test_graphics_arraybuffer_007.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// ArrayBuffer 2 bit - https://github.com/espruino/Espruino/issues/301
var LCD = Graphics.createArrayBuffer(8,8,2);
var LCD = Graphics.createArrayBuffer(8,8,2,{msb:false});

LCD.setColor(0);
LCD.fillRect(0,1,1,6);
Expand Down
45 changes: 45 additions & 0 deletions tests/test_graphics_arraybuffer_bitDepths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Graphics.prototype.dump = function(){
var s = "";
var n = 0;
for (var y=0;y<this.getHeight();y++) {
s+="\n";
for (var x=0;x<this.getWidth();x++)
s+=this.getPixel(x,y)?"#":" ";
}
return s;
}
Graphics.prototype.print = function(){
print("`"+this.dump()+"`");
}

var bits = [1,2,4,8,16,24];
result = 1;
bits.forEach(bpp => {
console.log("Testing BPP ",bpp);
var g = Graphics.createArrayBuffer(32,8,bpp,{msb:true});
if (E.getAddressOf(g.buffer)==0) throw new Error("Not a flat array! can't test properly");
var c = g.getColor();
g.drawLine(0,0,32,32);
for (var i=0;i<8;i++) g.fillRect(9+i,i, 32-i,i); // test fillRect at different offsets
//if (bpp<=4) g.print();

if (bpp==1 && btoa(g.buffer)!="gH///0A///8gH//+EA///AgH//gEA//wAgH/4AEA/8A=") { result = 0; print("Data stored wrong"); }
if (bpp==2 && btoa(g.buffer)!="wAA///////8wAA///////wwAA//////8AwAA//////AAwAA/////wAAwAA////8AAAwAA////AAAAwAA///wAA==") { result = 0; print("Data stored wrong"); }
if (bpp==4 && btoa(g.buffer)!="8AAAAA///////////////w8AAAAA//////////////8A8AAAAA/////////////wAA8AAAAA////////////AAAA8AAAAA//////////8AAAAA8AAAAA/////////wAAAAAA8AAAAA////////AAAAAAAA8AAAAA//////8AAAA=") { result = 0; print("Data stored wrong"); }
if (bpp==8 && btoa(g.buffer)!="/wAAAAAAAAAA//////////////////////////////8A/wAAAAAAAAAA/////////////////////////////wAA/wAAAAAAAAAA//////////////////////////8AAAAA/wAAAAAAAAAA////////////////////////AAAAAAAA/wAAAAAAAAAA/////////////////////wAAAAAAAAAA/wAAAAAAAAAA//////////////////8AAAAAAAAAAAAA/wAAAAAAAAAA////////////////AAAAAAAAAAAAAAAA/wAAAAAAAAAA/////////////wAAAAAAAA==") { result = 0; print("Data stored wrong"); }
if (bpp==16 && btoa(g.buffer)!="//8AAAAAAAAAAAAAAAAAAAAA/////////////////////////////////////////////////////////////wAA//8AAAAAAAAAAAAAAAAAAAAA//////////////////////////////////////////////////////////8AAAAA//8AAAAAAAAAAAAAAAAAAAAA/////////////////////////////////////////////////////wAAAAAAAAAA//8AAAAAAAAAAAAAAAAAAAAA////////////////////////////////////////////////AAAAAAAAAAAAAAAA//8AAAAAAAAAAAAAAAAAAAAA//////////////////////////////////////////8AAAAAAAAAAAAAAAAAAAAA//8AAAAAAAAAAAAAAAAAAAAA/////////////////////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAAAAAAAAAAAAAAAAAAAA////////////////////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAAAAAAAAAAAAAAAAAAAA//////////////////////////8AAAAAAAAAAAAAAAA=") { result = 0; print("Data stored wrong"); }
if (bpp==24 && btoa(g.buffer)!="////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////////////////////////////////////////////////////////////////////////////////AAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////////////////////////////////////////////////////////////////////////////AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAAAA") { result = 0; print("Data stored wrong"); }
//print(` if (bpp==${bpp} && btoa(g.buffer)!=${E.toJS(btoa(g.buffer))}) { result = 0; print("Data stored wrong"); }`);


// test the line reports as being in the right place
for (var y=0;y<8;y++)
for (var x=0;x<8;x++) {
var p = g.getPixel(x,y), exp = ((x==y)?c:0);
if (p!=exp) {
print(`Wrong pixel at ${x},${y} (${p} != ${exp})`);
result = 0;
}
}

});
6 changes: 3 additions & 3 deletions tests/test_graphics_asImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

let results = [];

let g = Graphics.createArrayBuffer(8,8,1);
let g = Graphics.createArrayBuffer(8,8,1,{msb:false});
g.drawLine(0,0,7,7);
let image = g.asImage();
results.push(E.toUint8Array(image.buffer).slice().join(",")=="128,64,32,16,8,4,2,1");
Expand All @@ -12,12 +12,12 @@ g.drawLine(0,0,7,7);
let image = g.asImage();
results.push(E.toUint8Array(image.buffer).slice().join(",")=="128,64,32,16,8,4,2,1");

let g = Graphics.createArrayBuffer(8,8,1,{zigzag:true});
let g = Graphics.createArrayBuffer(8,8,1,{zigzag:true,msb:false});
g.drawLine(0,0,7,7);
let image = g.asImage();
results.push(E.toUint8Array(image.buffer).slice().join(",")=="128,64,32,16,8,4,2,1");

let g = Graphics.createArrayBuffer(8,8,1);
let g = Graphics.createArrayBuffer(8,8,1,{msb:false});
g.drawLine(0,0,7,7);
let image = g.asImage("string");
results.push(E.toUint8Array(image).slice().join(",")=="8,8,1,128,64,32,16,8,4,2,1");
Expand Down
2 changes: 1 addition & 1 deletion tests/test_graphics_scroll.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
g = Graphics.createArrayBuffer(128,64,1);
g = Graphics.createArrayBuffer(128,64,1,{msb:false});
g.drawString("Hello");
var before = g.buffer.toString();
g.scroll(0,5);
Expand Down

0 comments on commit ad0f468

Please sign in to comment.