tgx: a 2D/3D graphics library for Teensy.

Another request/question. Would it be possible to have different x and y scaling on scalable blits, or if thats too slow or complicated, perhaps just scale in only 1 dimension?
 
Another request/question. Would it be possible to have different x and y scaling on scalable blits, or if thats too slow or complicated, perhaps just scale in only 1 dimension?

Hi,

Yes, it is already possible using the copyFrom() method which blits a source image over a destination image whatever there sizes may be (scaling is done using bilinear filtering). So for example, the code below
Code:
im2.getCrop(B2).copyfrom(im1.getCrop(B1))
blits the region of im1 delimited by B1 onto the region of im2 delimited by B2 and these 2 iBox2 B1 and B2 need not have the same size or same aspect ratio.

The getCrop() notation is a bit heavy so I have now added a alias with operator() so that im(B) is the same as im.getCrop(B). Thus, you could simply write:
Code:
im2(B2).copyfrom(im1(B1))
which is, I think, more readable. However, in order to use operator(), you will need to switch to the improved-drawing-primitives branch on github or wait for the branch merging in a few days...

Also, there are even more powerful methods available such as drawTextureQuad() which can not only blit between rectangular regions but even between quadrilateral regions (not necessarily rectangles) and still with bilinear filtering :). And then, there are also methods for doing quadrilateral mapping with gradient, with masking... I will try to make a wiki on github to list the available methods when I am done with the library new drawing primitives.
 
Last edited:
I've started porting my desktop graphics library over to Teensy 4.1 with the help of this wonderful library. I'm trying to draw with transparency. Do you have a simple of example of setting up the buffers?
 
I've started porting my desktop graphics library over to Teensy 4.1 with the help of this wonderful library. I'm trying to draw with transparency. Do you have a simple of example of setting up the buffers?

To answer my own question, you don't need to change how the buffers are setup. I was trying to draw transparency by changing the alpha channel of an RGB32 color. Instead you draw transparency by including an extra float to specify the amount of transparency for the various draw methods.
 
@vindar

Any chance on getting a ILI9488 driver that's compatible with gtx?

So I did manage to get gtx working with the ILI9488 driver, but it's incredibly slow. No double buffering, no differential buffers really makes things crawl.
 
So I did manage to get gtx working with the ILI9488 driver, but it's incredibly slow. No double buffering, no differential buffers really makes things crawl.
You might look at what I hacked up for ILI9341_t3n and ST7789_t3 code over in the thread:
https://forum.pjrc.com/threads/72526-ST7789-screen-driver?p=323650&viewfull=1#post323650

Note: There are currently limitations, like:

If you use DMA for updating the display, it will draw the whole screen. I had never added in the part of code to the DMA stuff to change it to use the clipping rectangle. I have off and on started to add that and may complete that, when I feel inspired. A lot of these other drivers are derived from this so for most would be easy to update.... ILI9488 is a different beast, it is derived, BUT in SPI must output 18 bit color, so there is two different ways... Use 16 bit colors in frame buffer, and we output to display it converts... OR 32 bits and the update goes direct, But for this requires EXTMEM due to the size, which is real slow...

Now if you use: updateScreen (not the async DMA), it does support the output being clipped by the clipping rectangle. So in the examples I hacked up, I did a quick and dirty code setup, that found the bounding rectangle of the part of the screen which changed, and then set the clipping rectangle to it and then called updateScreen.

Now if you were using the graphic primitives within the ILI9341_t3n instead of the gtx, the driver code can keep track of the union of all of the graphic primitives that are called and use that for the output clipping rectangle. But since you are doing that external, I did it brute force. Now if GTX computed it...

But with this type of automatic compute of differences. When I experimented with it back when doing it, I would probably structure the output code to understand this, so for example if your display code for a clock showed both a second hand and digital time, I would probably separate the drawing into two steps, one of the hands and one for the digital...
 
Thanks for the info Kurt. GTX in combination with with his ILI9431_T4 driver is crazy fast. I don't really have a good understanding of how it works, but I think it's figuring out which pixels have changed, and only updates those.

I would have been happy to stick with the ili9431, but it's just too small. The 4.0 inch ili9488 is a great size, but that 18bit color issue would make porting vladar's driver well above my pay grade. At very least it would dramatically shorten my life span increase my risk of a stroke.

As for using the ili9488 drawing primites, that might be another option but I've already invested about 100+ hours building a UI framework on top of tgx.

Such a pity. Having a nice 4-5inch touchscreen with per pixel access seems to still be out of reach.
 
There is also the display: https://www.adafruit.com/product/2050
Although it is 3.5" but 16 bits...

Don't know your setup, so don't know how hard it would be to adapt. I may try pulling out my ILI9488... If I can find it and see how the clock demos in the other thread adapts to it.

Again with my hacked version mentioned above, what I did was the tgx has it's own frame buffer, and the display has it's own display buffer.
And then I just do a simple compare. Note this is quick and dirty.
Code:
void update_display(bool force_whole_display) {
  if (!force_whole_display) {
    // look for bounding range of changes.
    uint16_t first_changed_x = tft.width();
    uint16_t last_changed_x = 0;
    uint16_t first_changed_y = tft.height();
    uint16_t last_changed_y = 0;

    for (uint16_t y = 0; y < tft.height(); y++) {
      uint16_t* ptftfb_y = &ib[y * tft.width()];
      uint16_t* pimage_newfb_y = &fb[y * tft.width()];
      uint16_t x;
      for (x = 0; x < tft.width(); x++) {
        // find first if any change in line.
        if (ptftfb_y[x] != pimage_newfb_y[x]) {
          if (first_changed_y == tft.height()) first_changed_y = y;
          last_changed_y = y;
          if (x < first_changed_x) first_changed_x = x;
          break;
        }
      }
      // find last change in line
      if (x < tft.width()) {
        for (x = tft.width() - 1; x > last_changed_x; x--) {
          if (ptftfb_y[x] != pimage_newfb_y[x]) {
            last_changed_x = x;  //
            break;
          }
        }
      }
    }
    // now have the bounds
    tft.setClipRect(first_changed_x, first_changed_y, (last_changed_x - first_changed_x) + 1, (last_changed_y - first_changed_y) + 1);
    static uint8_t debug_count = 20;
    if (debug_count) {
      Serial.printf("(%u, %u), (%u, %u)\n", first_changed_x, last_changed_x, first_changed_y, last_changed_y );
      debug_count--;
    }

  } else {
    tft.setClipRect();
  }
  memcpy(ib, fb, sizeof(fb));
  tft.updateScreen();  // simply update.
}
His code probably works sort of similar. Except maybe it does not find the smallest area that encompasses all of the changed colors. He may look for sub portions of the display that changed and update each sub portion individually. I have not totally looked through his code as ...

But from the external it looks like some of the stuff for doing these partial screen updates using DMA, he is needing additional buffer(s) (memory) to copy pixes from each row of the update of the rectangle into the frame buffer as to have it all contiguous, probably uses two of them to make it easier to chain the two together with ISR calls when each one completes as to maybe fill in with the next set of data,, Note this is similar to stuff we are already doing in the ILI9488 code base, but instead of doing the copy to make the memory contiguous, we are doing it to convert our 16 bit colors into 18 bit colors.
 
Double buffering with 32bit or 16bit color at 480x320 just isn't going to work on T4.1--not enough memory.

I have no shortage of digital pins for this project, so I might be better off using a parallel interface.
 
Last edited:
Depends, if you look at other thread today on ili9488 I have hacked up one example and it runs with problems…

But you can not put both frame buffers in dmamem. Won’t fit. But one in dma and other in DTCM I.e. ram fit. And with t4.1 you can solder memory to bottom and use that
 
I have 2 PSRAM chips on this device which I can use, but it's slow. Putting one buffer in DMAMEM and the other in normal ram, doesn't leave a whole lot of RAM1 for my UI. I'll see if I can do some changes to free up more RAM1.
 
@yeahtuna @vindar,

There might also be other options as well when using the ILI9488.

As you mentioned, you potentially could try to run with 16 bit writes using parallel interface. Note the ILI9488_t3 does not support this. But this would not reduce the size of buffers for double buffering.

But another possibility is the ILI9488_t3 code has the ability to use an 8 bit framebuffer with a palette. But this only allows you to use 256 colors. Not sure how well that would work with whatever your desired interface is. Also don't know if TGX could also support working in 8 bit color mode. I did that originally to still allow these devices to work with a frame buffer on the T3.5/6, Decided to try it as for example the RA8875 displays that I was playing with at the time, with the higher resolution, did not support 16 bit color mode if you turned on the 2 buffer mode, and for some of the stuff I was playing with did not look too bad.

But again that depends on what your needs are.
 
An 8 bit color palette would probably work for me. I tried very hard to get my ram1 usage down enough to fit a 480x32 16bit buffer, but couldn't do it. Lots of space left in ram2. Is it possible to have a buffer that spans across ram1 and ram2?
 
The current code (ILI9488_t3) does not support frame buffer being split into two chunks. Have not looked at this library. I know at one point I hacked up a version of the ILI9341_t3n library to run on ESP32 and I needed to handle the split as their code like malloc() would not give me one chunk large enough to work, but did with two...
 
Another quick update: As I mentioned in the other thread about ST77xx and the like.

I did some updates to the ILI9488_t3 code yesterday to add in the clipping and dirty region code from the ILI9431_t3n.

I updated my two hacked up copies of the 2d examples that can be configured for either of the 3 displays.
On the ILI9481, I configured the AntiAliasedClock to leave the graphic library FB as ILI9341 size and then when displaying I told it to center the image with the writeRect call.
On this sketch it worked pretty well.

This morning, I updated the CrazyClock sketch to do the same, and there are some interesting issues I am seeing that I will try to later debug. There could be some off by one bugs or the like maybe...
Screenshot.png

And other things as you can see, but mostly works. I may see what happens if I tell it in the library FB to go full size...

I included this updated sketch the other one is in the other thread.
 

Attachments

  • CrazyClock-230413a.zip
    166.6 KB · Views: 59
Quick update: Fixed the off by 1 bug in ILI9488_t3 library. Libraries updated.

I also experimenting with the crazyClock updated to full size of ILI9488
Had to move the clock face to PROGMEM to get enough memory space.

Also added extra section in loop that scaled up the clock face.

But it is interesting that the full screen is not getting erased.

Screenshot.png

As you can see in the above picture. But the interesting thing is, that the whole screen is not getting erased.

Not sure why yet. Problem in our driver code? Or issues in the library.
Note: This happens in all of the frames...

In example I added the code like:
Code:
  #if defined(USE_ILI9488)
  em = 0;
  float scale = 320.0/240.0;
  while ((t = em) < 10000) {
    im.fillScreen(RGB565_Black);
    drawClock(0, scale);
    drawSmallHand(100 + 360 * sin(t / 1500.0f), scale);
    drawLongHand(500 * cos(t / 5000.0f), scale);
    update_display(false);
    yield();  // to keep the board responsive
  }
  #endif
And I would expect that the whole 480x320 display would be black...
Note, my earlier debug code cycled through a couple of colors to make sure the display was starting up correctly and the last color was BLUE
 

Attachments

  • CrazyClock_ili9341_t3x-230413a.zip
    168 KB · Views: 58
Quick question, if anyone is listening...

Wondering about drawing transparency. Is this only supported in RGB32? i.e. not supported in RGB565?

I was thinking about trying to draw the clock face background outside of the actual clock as transparent. But looking so far at the code
and the clock examples, where the hands were in RGB32.
Obviously could probably convert the clock face to this format, but was wondering if there was support to do this with RGB565, where potentially one
could set what color is the transparent color in that mode...

Just wondering and goofing off
 
Quick question, if anyone is listening...

Wondering about drawing transparency. Is this only supported in RGB32? i.e. not supported in RGB565?

I was thinking about trying to draw the clock face background outside of the actual clock as transparent. But looking so far at the code
and the clock examples, where the hands were in RGB32.
Obviously could probably convert the clock face to this format, but was wondering if there was support to do this with RGB565, where potentially one
could set what color is the transparent color in that mode...

Just wondering and goofing off

Hi @KurtE
was just looking at colors.cpp and looks like there may be support for RGB565 transparency - not sure:
Code:
        /**
         * alpha-blend `fg_col` over this one with a given opacity in the range 0.0f (fully transparent)
         * to 1.0f (fully opaque).
         * 
         * @param   fg_col  The foreground color.
         * @param   alpha   The opacity/alpha multiplier in [0.0f,1.0f].
        **/
        inline void blend(RGB565 fg_col, float alpha)
            {
            blend256(fg_col, (uint32_t)(alpha * 256));
            }



        /**
         * alpha-blend `fg_col` over this one with a given opacity in the integer range 0 (fully
         * transparent) to 256 (fully opaque).
         *
         * @param   fg_col  The foreground color.
         * @param   alpha   The opacity/alpha multiplier in [0,256].
        **/
        inline void blend256(const RGB565 & fg_col, uint32_t alpha)
            {       
            const uint32_t a = (alpha >> 3); // map to 0 - 32.
            const uint32_t bg = (val | (val << 16)) & 0b00000111111000001111100000011111;
            const uint32_t fg = (fg_col.val | (fg_col.val << 16)) & 0b00000111111000001111100000011111;
            const uint32_t result = ((((fg - bg) * a) >> 5) + bg) & 0b00000111111000001111100000011111;
            val = (uint16_t)((result >> 16) | result); // contract result
            }

but I think that is only forground to background.
 
re: transparency.

if you're using "im" for the tgx framebuffer name, and "tft" for the normal ILI9341 graphics commands, many of the primitives are the same, except you put a float between 0 and 1 at the end (after the color). it works with 16 bit 565 color, but low transparency wont register, especially on red and blue that only have 32 levels.

instead of tft.drawLine(x,y,x2,y2,color);
put im.drawLine(x,y,x2,y2,color,0.5f); for half transparency.

you can also do animated fade out tricks with drawing a semi transparent rectangle of low transparency over an area on each frame instead of completely blacking it out.
 
I did some hacking which I had another quick and dirty sketch convert the clock to RGB32...
View attachment xxx-230415a.zip

I had the code scan the lines trying to convert all 0 colors at start and end of line to be C(0,0,0) to be transparent, and the stuff
in between convert to RGB32, and did Serial.printf ... of the data.

I then hacked up the Antique clock to either use the origin file or this one...
View attachment CrazyClock_ili9341_t3x-230415a.zip

Screenshot.png

But as you can see not fully successful yet as some along the edge look like they were not (0,0,0)...

But interesting hacking
 
I did a little more hacking on my conversion sketch mentioned in previous post.

To use a threshold on the color to choose where the outside edges of the clock are...
Could post whole sketch again, but it was simply changing the code like:
Code:
#define COLOR_VAL_TEST 2
#if COLOR_VAL_TEST == 0 
void convert_to_rgb32() {
  for (int y = 0; y < 234; y++) {
    // for each row find each of the values
    // Lets find the first none 0 value in this row

    int x_first;
    const RGB565 *row_data = &antique_clock_data[y * 234];
    for (x_first = 0; x_first < 234; x_first++) {
      if (row_data[x_first].val != 0) break;  // found some data.
      if ((x_first & 0xf) == 0) Serial.println();
      Serial.print("C(0,0,0,0),");
    }
    if (x_first != 234) {
      // So row is not completely empty. So lets find the last non-zero value
      int x_last;
      for (x_last = 233; x_last >= x_first; x_last--) {
        if (row_data[x_last].val != 0) break;  // found some data.
      }
      // now lets convert each of the actual data elements to RGB32
      for (; x_first <= x_last; x_first++) {
        if ((x_first & 0xf) == 0) Serial.println();
        RGB565 c = row_data[x_first];
        Serial.printf("C(%d,%d,%d,255),",
                      (uint8_t)((((uint8_t)c.R) << 3) | (((uint8_t)c.R) >> 2)),
                      (uint8_t)((((uint8_t)c.G) << 2) | (((uint8_t)c.G) >> 4)),
                      (uint8_t)((((uint8_t)c.B) << 3) | (((uint8_t)c.B) >> 2)));
      }
      for (; x_first < 234; x_first++) {
        if ((x_first & 0xf) == 0) Serial.println();
        Serial.print("C(0,0,0,0),");
      }
    }
    Serial.println(); // show breaks for rows
  }
}
#else
// this version will look at the colors and stop if any one over threahold. 
void convert_to_rgb32() {
  for (int y = 0; y < 234; y++) {
    // for each row find each of the values
    // Lets find the first none 0 value in this row

    int x_first;
    const RGB565 *row_data = &antique_clock_data[y * 234];
    for (x_first = 0; x_first < 234; x_first++) {
      RGB565 c = row_data[x_first];
      if ((c.R > COLOR_VAL_TEST) || (c.G > COLOR_VAL_TEST) || (c.B > COLOR_VAL_TEST)) break; 
      if ((x_first & 0xf) == 0) Serial.println();
      Serial.printf("C(%d,%d,%d,0),",
                    0,0,0);
/*                      (uint8_t)((((uint8_t)c.R) << 3) | (((uint8_t)c.R) >> 2)),
                    (uint8_t)((((uint8_t)c.G) << 2) | (((uint8_t)c.G) >> 4)),
                    (uint8_t)((((uint8_t)c.B) << 3) | (((uint8_t)c.B) >> 2))); */
    }
    if (x_first != 234) {
      // So row is not completely empty. So lets find the last non-zero value
      int x_last;
      for (x_last = 233; x_last >= x_first; x_last--) {
        RGB565 c = row_data[x_last];
        if ((c.R > COLOR_VAL_TEST) || (c.G > COLOR_VAL_TEST) || (c.B > COLOR_VAL_TEST)) break; 
      }
      // now lets convert each of the actual data elements to RGB32
      for (; x_first <= x_last; x_first++) {
        if ((x_first & 0xf) == 0) Serial.println();
        RGB565 c = row_data[x_first];
        Serial.printf("C(%d,%d,%d,255),",
                      (uint8_t)((((uint8_t)c.R) << 3) | (((uint8_t)c.R) >> 2)),
                      (uint8_t)((((uint8_t)c.G) << 2) | (((uint8_t)c.G) >> 4)),
                      (uint8_t)((((uint8_t)c.B) << 3) | (((uint8_t)c.B) >> 2)));
      }
      for (; x_first < 234; x_first++) {
        if ((x_first & 0xf) == 0) Serial.println();
        RGB565 c = row_data[x_first];
        Serial.printf("C(%d,%d,%d,0),",
                      0,0,0);
/*                      (uint8_t)((((uint8_t)c.R) << 3) | (((uint8_t)c.R) >> 2)),
                      (uint8_t)((((uint8_t)c.G) << 2) | (((uint8_t)c.G) >> 4)),
                      (uint8_t)((((uint8_t)c.B) << 3) | (((uint8_t)c.B) >> 2))); */
      }
    }
    Serial.println(); // show breaks for rows
  }
}
#endif

Then running it and replacing the data in previous post with generated data in antique_clock_RGB32.cpp file (have to manually edit... remove trailing ,...

And now running on two Micromod boards one with ILI9488 and other with ST7789.
Screenshot.jpg

Probably done for now plaing with TGX and the like.

Note: I am getting some compiler warnings in the improved-drawing-primitives branch:
With this quick and dirty code:
Code:
  const RGB32 background_color = RGB565_Black;
  switch (loop_count) {
    case 1: background_color = RGB32_Green; break;
    case 2: background_color = RGB32_Orange; break;
    case 3: background_color = RGB32_Silver; break;
  }
Sorry again quick and dirty, would normally setup static table with colors, but...
Code:
C:\Users\kurte\Documents\Arduino\Teensy Tests\CrazyClock_ili9341_t3x\CrazyClock_ili9341_t3x.ino: In function 'void loop()':
C:\Users\kurte\Documents\Arduino\Teensy Tests\CrazyClock_ili9341_t3x\CrazyClock_ili9341_t3x.ino:314:32: warning: passing 'const tgx::RGB32' as 'this' argument discards qualifiers [-fpermissive]
  314 |     case 1: background_color = RGB32_Green; break;
      |                                ^~~~~~~~~~~
In file included from c:\Users\kurte\Documents\Arduino\libraries\tgx\src/tgx.h:38,
                 from C:\Users\kurte\Documents\Arduino\Teensy Tests\CrazyClock_ili9341_t3x\CrazyClock_ili9341_t3x.ino:41:
c:\Users\kurte\Documents\Arduino\libraries\tgx\src/Color.h:1354:16: note:   in call to 'constexpr tgx::RGB32& tgx::RGB32::operator=(const tgx::RGB32&)'
 1354 |         RGB32& operator=(const RGB32&) = default;
      |                ^~~~~~~~
C:\Users\kurte\Documents\Arduino\Teensy Tests\CrazyClock_ili9341_t3x\CrazyClock_ili9341_t3x.ino:315:32: warning: passing 'const tgx::RGB32' as 'this' argument discards qualifiers [-fpermissive]
  315 |     case 2: background_color = RGB32_Orange; break;
      |                                ^~~~~~~~~~~~
In file included from c:\Users\kurte\Documents\Arduino\libraries\tgx\src/tgx.h:38,
                 from C:\Users\kurte\Documents\Arduino\Teensy Tests\CrazyClock_ili9341_t3x\CrazyClock_ili9341_t3x.ino:41:
c:\Users\kurte\Documents\Arduino\libraries\tgx\src/Color.h:1354:16: note:   in call to 'constexpr tgx::RGB32& tgx::RGB32::operator=(const tgx::RGB32&)'
 1354 |         RGB32& operator=(const RGB32&) = default;
      |                ^~~~~~~~
C:\Users\kurte\Documents\Arduino\Teensy Tests\CrazyClock_ili9341_t3x\CrazyClock_ili9341_t3x.ino:316:32: warning: passing 'const tgx::RGB32' as 'this' argument discards qualifiers [-fpermissive]
  316 |     case 3: background_color = RGB32_Silver; break;
      |                                ^~~~~~~~~~~~
In file included from c:\Users\kurte\Documents\Arduino\libraries\tgx\src/tgx.h:38,
                 from C:\Users\kurte\Documents\Arduino\Teensy Tests\CrazyClock_ili9341_t3x\CrazyClock_ili9341_t3x.ino:41:
c:\Users\kurte\Documents\Arduino\libraries\tgx\src/Color.h:1354:16: note:   in call to 'constexpr tgx::RGB32& tgx::RGB32::operator=(const tgx::RGB32&)'
 1354 |         RGB32& operator=(const RGB32&) = default;
      |                ^~~~~~~~

Now off to playing with some other stuff...
 
Actually in this case, I think some of this sketch looks more fun with this display:
Sorry the picture was not great:
Screenshot.png

Edit: Plus could do more work on getting it centered a bit better

Update also have it working on our HX8357_t3n library (Adafruit display is the main one of these I know of )
Screenshot.png
 

Attachments

  • CrazyClock_ili9341_t3x-230417a.zip
    270.2 KB · Views: 525
Last edited:
Back
Top