mrcodetastic / ESP32-HUB75-MatrixPanel-DMA

An Adafruit GFX Compatible Library for the ESP32, ESP32-S2, ESP32-S3 to drive HUB75 LED matrix panels using DMA for high refresh rates. Supports panel chaining.
MIT License
932 stars 206 forks source link

Pixel line skew between top and bottom panel halves when horizontal scrolling - P3 #338

Closed xemjeff closed 1 year ago

xemjeff commented 1 year ago

When horizontal scrolling, the P3 (64x32) display shows an offset between panel top and bottom at the halfway mark. I have 3 panels chained. The offset increases the faster the scroll. In the image below, I am writing out the lower case letter 'L' with a delay of 4ms between scroll shifts (one pixel to the left each cycle, blank the screen, redraw the text).

Seems like it's related to #133, but it does not happen with a static display - and it gets worse the faster the scroll.

Any thoughts on what to adjust or where to look? pixelSkew-2

The P3 panel has this chip: CHIPONE ICN2037BP

Here's a link to the docs for the chip: https://olympianled.com/wp-content/uploads/2021/05/ICN2037_datasheet_EN_2017_V2.0.pdf

I tried changing the driver to ICN2038S, but that does not help - same effect.

DarrylStrong commented 1 year ago

This is due to the panels being two seperate halves top and bottom. As the eye follows the text across the screen there is a difference where the scans are. Top line will be in the same position as the top of the bottom half.

You can reduce the effect by getting the scan rate as high as possible (increase the clock frequency and reduce the colour depth) but it will still be there.

The other option for scrolling is to do bit manipulation on the DMA buffers themselves, I have modified the library to make the buffers public so I can then rotate the colour bits without moving the row select bits to facilitate scrolling without having to redraw the whole display.

mrcodetastic commented 1 year ago

Hi @xemjeff, I've noticed this as well.

What version of the library are you using? 2.0.7 or what's in the repository?

What ESP32 hardware variant?

Oh, and are you using double buffering?

xemjeff commented 1 year ago

This is due to the panels being two seperate halves top and bottom. As the eye follows the text across the screen there is a difference where the scans are. Top line will be in the same position as the top of the bottom half.

You can reduce the effect by getting the scan rate as high as possible (increase the clock frequency and reduce the colour depth) but it will still be there.

The other option for scrolling is to do bit manipulation on the DMA buffers themselves, I have modified the library to make the buffers public so I can then rotate the colour bits without moving the row select bits to facilitate scrolling without having to redraw the whole display.

Thanks for your response @DarrylStrong. As you say the first rows (top half, bottom half) would be aligned. But that's not what we see here. Unless I misunderstand you, row 0 and row 16 are shifted in and latched at the same time.

DarrylStrong commented 1 year ago

Yes they are shifted at the same time but row 15 isn't, it is one full scan behind Hence the step.

xemjeff commented 1 year ago

Version (from library.json) is 2.0.6. ESP32 VROOM 32D

I am using flipBuffer(), so I guess that GFX version of double buffering. Is there perhaps double buffering also in the library?

mrcodetastic commented 1 year ago

Can you post a test sketch?

xemjeff commented 1 year ago

@DarrylStrong OK, I don't understand what you mean - forgive my ignorance. Row 0 to 15 are sycn'd, Rows 16 to 31 look like one pixel ahead. But at slower speeds (40 ms between updates), it looks more like 1/2 pixel ahead. And statically, there is no shift.

xemjeff commented 1 year ago

@mrfaptastic ok - I'll put something simple together to show the effect.

mrcodetastic commented 1 year ago

This is due to the panels being two seperate halves top and bottom. As the eye follows the text across the screen there is a difference where the scans are. Top line will be in the same position as the top of the bottom half.

It's not human, there seems to be some weird delay between what is pumped out to the RGB1 vs RGB2 pins.

I don't know if there's some hidden byte ordering issue or DMA update internal silicon delay. Or perhaps the boards are slow to latch the bottom half vs. top.

DarrylStrong commented 1 year ago

it is because your eye tracks across the leds as the text moves. That is what causes scrolling text on 7 row signs to tilt.

DarrylStrong commented 1 year ago

This is due to the panels being two seperate halves top and bottom. As the eye follows the text across the screen there is a difference where the scans are. Top line will be in the same position as the top of the bottom half.

It's not human, there seems to be some weird delay between what is pumped out to the RGB1 vs RGB2 pins.

I don't know if there's some hidden byte ordering issue or DMA update internal silicon delay. Or perhaps the boards are slow to latch the bottom half vs. top.

I have been working in the LED display industry for thirty years. Static driven led matrices smear when scrolled, row scanned tilt and column scanned get shorter. This effect is something we had to work with on one of our 16 row boards which was scanned 8:1. We had to artificially shift one set of 8 rows sideways to remove the step.

board707 commented 1 year ago

This is just a low scan rate. I have seen this many times. To avoid it you have to make scan rate significantly faster than scrolling. @xemjeff used 40ms as scroll delay, so your scroll rate is 25 fps. To eliminate the effect he needs update the panel 200-300 times at second.

Addition @xemjeff I read above that you tried to scroll with a delay of 4ms - this is too fast. In order for the picture to remain without distortion, with such a scroll, it is necessary to update the image more than 1000 times per second. I think you should reduce the scroll speed to 20-30ms inter-scroll delay

xemjeff commented 1 year ago

Here's a test sketch. The skew only happens at the mid panel. I can run 6,8 ms and there is no skew if I display on the top half or the bottom half - as long as it does not cross the middle row.

@board707 the skew still happens at 20-40 ms.

@mrfaptastic this is using double buffering from the library (set to "true")

Thanks to everyone for suggestions. Hopefully the test sketch will help. PixelSkew.zip

mrcodetastic commented 1 year ago

This is just a low scan rate. I have seen this many times. To avoid it you have to make scan rate significantly faster than scrolling. @xemjeff used 40ms as scroll delay, so your scroll rate is 25 fps. To eliminate the effect he needs update the panel 200-300 times at second.

Right, so basically the DMA buffer is being outputted slower to the panels than the CPU is updating the DMA buffer and causing essentially tearing. Hmmmm.

DarrylStrong commented 1 year ago

So this is why I don't see it as badly then as I move the dma buffers for scrolling rather than redrawing the whole screen.

xemjeff commented 1 year ago

Why is the tearing always at the mid-line? The data are shift loaded in parallel, the OE and LATCH are used for both. From what I see, the bottom is updated before the top and the skew distance changes with scrolling speed.

If the CPU is updating the buffer at varying rates based on scroll rate, wouldn't the tear change row location?

xemjeff commented 1 year ago

I think I understand what's going on: The lower and upper panels are drawn at the same time. But the line is moving. It takes a certain amount of time (t_k) to move through the 16 rows. What we "see" are two slanted lines, and of course they don't connect. (see figure below). The faster the perceived movement, the greater the perceived slant. I tool slow-motion video of the dislplay and it's fine, The pixels on top and bottom line up.

SkewMidLine

If this correct, then it's not tearing. I'm not sure if increasing the update rate would help. Let me know if you concur.

DarrylStrong commented 1 year ago

That's exactly what I was trying to explain.

Hence why we had to offset the bottom half of our display by one pixel.

xemjeff commented 1 year ago

@DarrylStrong Yes - I understand now - it just took me a while :-)

I tried offsetting the bottom panel by one pixel. That works at the fast speeds. Then of course, as we slow down the scroll, we get the reverse problem - skewing in the other direction.

I'm curious why manipulating the DMA buffer would make any difference. This would still result in the slant effect, no?

Added: We could, I suppose, update the rows top to bottom for the top half, and bottom to top for the bottom half. They would then meet in the middle at the same time. But that might look a bit weird, and I'm not sure the display supports that.

DarrylStrong commented 1 year ago

I didn't explain myself very well 😊

It is just faster manipulating the buffers, I am using the library for 384x64 displays so the redraw takes a while 😉

DarrylStrong commented 1 year ago

I didn't explain myself very well 😊

It is just faster manipulating the buffers, I am using the library for 384x64 displays so the redraw takes a while 😉

It was also helped when we did this pixel shift that the scroll speed was synchronised with the scan rate.

board707 commented 1 year ago

The lower and upper panels are drawn at the same time. But the line is moving. It takes a certain amount of time (t_k) to move through the 16 rows. What we "see" are two slanted lines, and of course they don't connect. (see figure below). The faster the perceived movement, the greater the perceived slant. I tool slow-motion video of the dislplay and it's fine, The pixels on top and bottom line up.

But I am wondering why the double buffering do not fix this artefacts. As far as understand it, with double buffering whole picture should updated in one time.

DarrylStrong commented 1 year ago

It is written at the same time but your eyes follow the image across so see the columns in different positions as the text scrolls. Of course there are only two rows on at a time... Refer to the fantastic diagram above 😊

xemjeff commented 1 year ago

expanding on @DarrylStrong's explanation:

Your eye is following the line as is moves. The pixels are displayed one row at a time. (in my case one pixel per row) From one row to the next, your eye has moved, following the line. This means that the next row is shifted on your retina by some small amount, Same for the next pixel. This results in the perception of a slanted line.

mrcodetastic commented 1 year ago

Version (from library.json) is 2.0.6. ESP32 VROOM 32D

I am using flipBuffer(), so I guess that GFX version of double buffering. Is there perhaps double buffering also in the library?

That's your problem. You need to use 2.0.7

If you are using double buffering, and you give a few milliseconds from when you stop drawing to when you call 'flipDMABuffer', then you shouldn't have this issue... but only if you're using 2.0.7

We had this problem previously and I found a way to fix it in 2.0.7

Don't use the latest git version either as it's probably broken again.

board707 commented 1 year ago

Your eye is following the line as is moves...

If it was only an optical effect, we would not see a shift in the photo, as in the first message. I just test scrolling in the chain of two 64x32 RGB matrices with scroll delay 5ms and picture update 200fps - i don't see anything similar to your picture. The scrolling letters is absolutely straight in vertical.

xemjeff commented 1 year ago

@mrfaptastic I tried 2.0.7, and introduced 2ms and 4ms delay before flipping the buffer. (dbuff = true). Same shift effect.

@board707 : did you test with the test sketch I provided, or some other implementation? If different, could you post that? what version of the lib? 2.0.6, 2.07, etc? where do you set 200FPS - is that the min_refresh_rate? I tried 60, 85, 200 and saw no difference.

Added: I just tested with 2 chained boards instead of 3 (64x32). Same problem.

board707 commented 1 year ago

did you test with the test sketch I provided, or some other implementation?

Sorry, I tested just to make sure it's not an optical effect. I used completely different environment - 2x 64x32 RGB panels with RP2040 board and specific library. (This is how the letters looks like when scrolling from left to right at a speed of 5ms/pixel. ) P_20221026_010240_small

xemjeff commented 1 year ago

@board707 has a good point. Why would it show up skewed in the slo-mo video?

I changed the code to draw a 2 pixel vertical line across the mid row. When I shoot that video in slow motion, I see the two pixels skewed.

twoPixelVLine

xemjeff commented 1 year ago

I ran another test:

Two pixel, as above, but from left to right rather than right to left. The skew is now reversed (the 2nd pixel now to the right of the first pixel).

My conclusions.

I am curious as to why @board707 is not seeing this effect.

board707 commented 1 year ago
  • The skew is because the 2nd pixel is drawn right after the first, but in the next column as the scan begins again.

I think you are right about the nature of the effect. Sometimes I also see something similar, but it is much weaker and rarely appears. Perhaps we need to take into account the shooting parameters of the camera taking photos.

By the way, decreasing the frame rate from 200 to 100 fps does not affect the picture at all in my case - I don't see the effect, so my previous suggestions were wrong.

mrcodetastic commented 1 year ago

Edit: This comment is wrong, so please ignore.

Because the delta between the top half of the panel and the bottom half is straight per the photo @xemjeff posted in the opening post, I think it's to do with the fact:

1) The Adafruit GFX library when updating any drawing activities, starts from pixel (y,x)(0,0) then scans down from left to right(0,63), (1,0) (1,63) etc. 2) By the time AdaFruit GFX updates pixels on the bottom half (i.e rows > half the height of the panel) by hitting this condition in the code: https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA/blob/69686a374702613432541a135b202813aea1de8a/src/ESP32-HUB75-MatrixPanel-I2S-DMA.cpp#L302 .... then the DMA buffer has probably already been output to the HUB75 panel.

I can't do much about this asynchronous timing issue between Adafruit GFX, this library updating SRAM and the hardware DMA engine pumping out the SRAM to the GPIOs.

However, if this is the possible root cause, I just noticed that the AdafruitGFX library has a startDraw and endDraw function that is called for every big draw primative (like drawCircle/text). So perhaps I can implement this into the library and block flipping of the DMA buffer until routines such as 'drawline' have completed. Might help.

Edit: @xemjeff, double checking your using this library as it typically is used, by leveraging the AdafruitGFX right?

xemjeff commented 1 year ago

Because the delta between the top half of the panel and the bottom half is straight per the photo @xemjeff posted in the opening post, I think it's to do with the fact:

  1. The Adafruit GFX library when updating any drawing activities, starts from pixel (y,x)(0,0) then scans down from left to right(0,63), (1,0) (1,63) etc.
  2. By the time AdaFruit GFX updates pixels on the bottom half (i.e rows > half the height of the panel) by hitting this condition in the code: https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA/blob/69686a374702613432541a135b202813aea1de8a/src/ESP32-HUB75-MatrixPanel-I2S-DMA.cpp#L302

    .... then the DMA buffer has probably already been output to the HUB75 panel.

I can't do much about this asynchronous timing issue between Adafruit GFX, this library updating SRAM and the hardware DMA engine pumping out the SRAM to the GPIOs.

However, if this is the possible root cause, I just noticed that the AdafruitGFX library has a startDraw and endDraw function that is called for every big draw primative (like drawCircle/text). So perhaps I can implement this into the library and block flipping of the DMA buffer until routines such as 'drawline' have completed. Might help.

Edit: @xemjeff, double checking your using this library as it typically is used, by leveraging the AdafruitGFX right?

I am using the Adafruit library, but for one of the simple examples (drawing a two pixel vertical line), I am calling this function:

    virtual void drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color){
      uint8_t r, g, b;
      color565to888(color, r, g, b);
      vlineDMA(x, y, h, r, g, b);
    }

which I think bypasses Adafruit GFX. It still exhibits the bottom panel offset (slightly advanced).

mrcodetastic commented 1 year ago

What happens if you don't use the 'fast' line drawing function? I'll have to look into these DMA hline and vline functions as @vortigont coded these a while back.

vortigont commented 1 year ago

I've been summoned :)

yes, those fast functions are overloading adafruit's ones, but works on DMA buffer directly to bypass per-pixel logic. Was quite efficient for long lines and rectangles as I remember it.

Quite an interesting topic. Looks like many things has changed, I was missing you guys :)

and it gets worse the faster the scroll.

What do you mean? the tearing becomes more than 1 px or you see the ghosting or smearing? The pic in the first message is very clear. Looks more like static pic than captured motion.

@mrfaptastic I tried 2.0.7, and introduced 2ms and 4ms delay before flipping the buffer. (dbuff = true). Same shift effect.

just a wild guess... introduce a delay right after flipDMABuffer() not before. And use at least 10 ms delay (assuming panel refresh rate is 100 Hz). The idea is as I see from the code flipDMABuffer() is now async, it means that you flip the buff and it STILL continues to draw the old buff till the end. If your delay is small enough to modify that same old buff with advanced pixel positions you'll end up with last row displayed wrong position if I understood this case right.

for the sake of clarity, it would be nice to draw a full height vertical line moving from left<>right with an increasing/decreasing speed. i.e. start with delay of 500-800 ms between redraw and go down to 0 with a step of <5 ms, if it's double buffering issue than I'm pretty sure you'll see some tearing not only in the middle of a screen somewhere near 2-3 periods of a refresh rate.

cheers!

mrcodetastic commented 1 year ago

That's exactly what I was trying to explain.

Hence why we had to offset the bottom half of our display by one pixel.

Yep. @DarrylStrong is on the mark here.

Tried to fiddle with the library to see if there's anything hacky that could help, but... obviously not.

I've been summoned :)

Your code is solid, so not to worry.

The library can't do much about human eyesight and 1/2 scan panels :-)

DarrylStrong commented 1 year ago

Thanks :)

The only way I can see of correcting this 'properly' is to sync the scrolling with the scanning and move the one half one pixel to the left to compensate for the lean. This will be quite a thing to master. Alternatively (as mentioned previously) high scan rate (reduce colour depth) will help.

I have a sideways scroll on our 64 pixel high displays as a transition effect and it splits into 4 sections when moving but as it is only there to draw attention to change it is not important to see this anomoly.

mrcodetastic commented 1 year ago

Not that I have tested this theory, but perhaps one should make sure the physical panel's scanning direction is better aligned to that of the program/sketch's graphics (if it's relatively consistent).

i.e. If one is going to display heaps of graphics that scrolls from left to right (either way), then don't have the panels scanning perpendiular to this (top down), have then scan left to right or right to left as well.

Note: This would require hacking the pixel mapping code of course, but could possibly be done with the virtual matrix library.

DarrylStrong commented 1 year ago

That would alleviate the 'tearing' for sure but might introduce a 'rippling' effect instead.

xemjeff commented 1 year ago

Sorry the raise the dead, but I just came back to this problem today. Looking through the code, I retested turning double buffering off, and the skew is GONE!

  mxconfig.double_buff = false;
  dma_display = new MatrixPanel_I2S_DMA(mxconfig);

but now, I get lots plenty of flickering. But I do wonder how does double buffering cause the skew. Is it the timing of the buffer flip?

@mrfaptastic If you are interested, I will recreate a simple sketch and re-post here with the double buffering on and off. I can also post videos. Let me know if you want to pick this up.

mrcodetastic commented 1 year ago

Wow.

What version of the code were you using? The git master or the latest 3.0.5 release?

A simple sketch should be sufficient. Thank you.

xemjeff commented 1 year ago

@mrfaptastic Here's a simple sketch, using a single P3 panel (64W x 32H). You will have to edit the pindefs.h file for your board configuration.

In main.cpp, change DOUBLE_BUFFER from true to false. With true, you see the shift/skew a the mid row. With false, no shift/skew.

pixelSkew2.zip

Thanks for taking a look.

I am using version 2.06. I recall that when I updated to versions 3.0.0 and later, my scrolling was much, much slower.

mrcodetastic commented 1 year ago

Please try again with the latest git version. The Skew with double buffering still exists as this is a optical illusion.

When turning off double buffering you don't see the skew as the whole panel is skewed because the drawing by the CPU is occuring as you see it, from top to bottom.

xemjeff commented 1 year ago

I just built from master branch. Same thing. No skew, shift or even apparent slant with double buffering off.

In the source for the ESP32-VirtualMatrixPanel-I2S-DMA.h file, I noticed several attempts (commented out) dealing with the timing of the buffer flip


    inline void flipDMABuffer() 
    {         
        if ( !m_cfg.double_buff) { return; }

        // while (active_gfx_writes) { } // wait a bit ?
      //  initialized = false;
          dma_bus.flip_dma_output_buffer( back_buffer_id ); 
    //    initialized = true;

        /*
        i2s_parallel_set_previous_buffer_not_free();       
        // Wait before we allow any writing to the buffer. Stop flicker.
        while(i2s_parallel_is_previous_buffer_free() == false) { }       

        i2s_parallel_flip_to_buffer(ESP32_I2S_DEVICE, back_buffer_id);        
        // Flip to other buffer as the backbuffer. 
        // i.e. Graphic changes happen to this buffer, but aren't displayed until flipDMABuffer() is called again.
        back_buffer_id ^= 1;        

        i2s_parallel_set_previous_buffer_not_free();       
        // Wait before we allow any writing to the buffer. Stop flicker.
        while(i2s_parallel_is_previous_buffer_free() == false) { }          
        */

    }
mrcodetastic commented 1 year ago

One thing we aren't testing in the example is the vertical line going from left to right and then right to left.

If the pixel offset is the same both directions then perhaps it is a data sync issue.

If the pixel offset changes the other way around then proves that it's an optical illusion to do with the row scanning. If that's the case then there's no fix other than to offset all fast coords in the bottom half of a panel.

xemjeff commented 1 year ago

@mrfaptastic : I've tested in both directions. The shift follows the direction. But I'm not convinced this is an optical illusion. If it were, then the first image in this thread (a photo still) would not capture the shift.

First, I'd like to understand more about how to time the buffer flip. In my app, I'm flipping buffers every frame, each time I redraw the display, shifting the column offset by 1. Should that be synced with the DMA transfer?

Second, we could change the order in which the A,B,C,D pattern is output with matching row data. The sequence now is row = [0,1,2,3 ..15] with RGB1 and the matching row at row+16 using RGB2. If we move this to start in the middle of the top and bottom, we would use [8,7,9,6,10,5,11,4,12,3,13,2,14,1,15,0] for RGB1 and again row+16 for RGB2. That we remove the time difference between rows 1 and 16 - they would be adjacent in the output sequence.

Let me know which direction (or both) I could follow. I'm happy to do the work with your guidance in a forked repo and issue a PR when it's working.

/Jeff

board707 commented 1 year ago

Should that be synced with the DMA transfer?

No, it's the responsibility of the library.

The shift follows the direction.

Is the shift still the same on whole height of the line? Or, are the both lines ( above and below middle-level) straight vertical?

xemjeff commented 1 year ago

@board707 This is hard to tell - but I see the lines are always vertical. I captured video with iPhone at 240fps (slo-mo), and the extracted frames using ffmpeg. Here is a sequence. ShiftSequence-240fps.zip

@mrfaptastic : Maybe we could capture this with a logic analyzer and provide a buffer switch GPIO output - high for buffer1, low for buffer2. Then look at the outputs of A,B,C,D and ensure that buffers only switch at transition from 1111 to 0000. If that were verified, then we would know the buffer switch is not the cause. Let me know what you think.

mrcodetastic commented 1 year ago

@xemjeff That would be usefull to double confirm it is an issue.

From a code perspective, one option is to start from scratch using this example that Espressif's Sprite_TM created (which was actually the genesis code for the creation of this library).

Hack it right back to the bare bones just to draw a line and see if the same issue happens as well:

https://www.esp32.com/viewtopic.php?f=17&t=3188

If I find time I will try look at this as an intellectual curiosity more than anything. A time sink this will be. Curious to see if we have this same issue on the ESP S2 and S3 devices now.

mrcodetastic commented 1 year ago

Actually, your buffer switch GPIO suggestion I'll have to incorporate into any new test case based on my comment above. So don't hack the library and bother doing this yourself.